From ea70b296ea944b6767a4e59e84d89b9173a34e9d Mon Sep 17 00:00:00 2001 From: exaby73 Date: Sun, 29 Dec 2024 21:14:51 +0530 Subject: [PATCH] feat!: Remove HTTP --- .../integration-test-single-server.yml | 12 - src/Authentication/BasicAuth.php | 17 - src/ClientBuilder.php | 2 +- src/Contracts/AuthenticateInterface.php | 6 - src/Contracts/FormatterInterface.php | 39 -- src/Databags/DriverConfiguration.php | 36 +- src/Databags/HttpPsrBindings.php | 133 ----- src/DriverFactory.php | 28 +- src/Enum/ConnectionProtocol.php | 8 - src/Formatter/OGMFormatter.php | 61 +- src/Formatter/Specialised/HttpMetaInfo.php | 124 ---- .../Specialised/JoltHttpOGMTranslator.php | 466 --------------- .../Specialised/LegacyHttpOGMTranslator.php | 536 ------------------ src/Formatter/SummarizedResultFormatter.php | 109 ---- src/Http/HttpConnection.php | 165 ------ src/Http/HttpConnectionPool.php | 135 ----- src/Http/HttpDriver.php | 206 ------- src/Http/HttpHelper.php | 220 ------- src/Http/HttpSession.php | 191 ------- src/Http/HttpUnmanagedTransaction.php | 171 ------ src/Http/RequestFactory.php | 56 -- src/ParameterHelper.php | 46 +- tests/Integration/ClientIntegrationTest.php | 9 +- 23 files changed, 31 insertions(+), 2745 deletions(-) delete mode 100644 src/Databags/HttpPsrBindings.php delete mode 100644 src/Formatter/Specialised/HttpMetaInfo.php delete mode 100644 src/Formatter/Specialised/JoltHttpOGMTranslator.php delete mode 100644 src/Formatter/Specialised/LegacyHttpOGMTranslator.php delete mode 100644 src/Http/HttpConnection.php delete mode 100644 src/Http/HttpConnectionPool.php delete mode 100644 src/Http/HttpDriver.php delete mode 100644 src/Http/HttpHelper.php delete mode 100644 src/Http/HttpSession.php delete mode 100644 src/Http/HttpUnmanagedTransaction.php delete mode 100644 src/Http/RequestFactory.php diff --git a/.github/workflows/integration-test-single-server.yml b/.github/workflows/integration-test-single-server.yml index bb8e65b3..d0432d1a 100644 --- a/.github/workflows/integration-test-single-server.yml +++ b/.github/workflows/integration-test-single-server.yml @@ -42,12 +42,6 @@ jobs: -e PHP_VERSION=${{ matrix.php }} \ -e CONNECTION=bolt://neo4j:testtest@neo4j \ client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration - - name: Test http:// - run: | - docker compose run \ - -e PHP_VERSION=${{ matrix.php }} \ - -e CONNECTION=http://neo4j:testtest@neo4j \ - client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration tests-v5: runs-on: ubuntu-latest strategy: @@ -81,9 +75,3 @@ jobs: -e PHP_VERSION=${{ matrix.php }} \ -e CONNECTION=bolt://neo4j:testtest@neo4j \ client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration - - name: Test http:// - run: | - docker compose run \ - -e PHP_VERSION=${{ matrix.php }} \ - -e CONNECTION=http://neo4j:testtest@neo4j \ - client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index 675e667c..7f6fb256 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -13,8 +13,6 @@ namespace Laudis\Neo4j\Authentication; -use function base64_encode; - use Bolt\protocol\V4_4; use Bolt\protocol\V5; use Bolt\protocol\V5_1; @@ -25,7 +23,6 @@ use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; use Psr\Log\LogLevel; @@ -43,20 +40,6 @@ public function __construct( private readonly ?Neo4jLogger $logger, ) {} - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface - { - $this->logger?->log(LogLevel::DEBUG, 'Authenticating using BasicAuth'); - $combo = base64_encode($this->username.':'.$this->password); - - /** - * @psalm-suppress ImpureMethodCall Request is a pure object: - * - * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message-meta.md#why-value-objects - */ - return $request->withHeader('Authorization', 'Basic '.$combo) - ->withHeader('User-Agent', $userAgent); - } - /** * @throws Exception * diff --git a/src/ClientBuilder.php b/src/ClientBuilder.php index cde9c86e..0ec4b497 100644 --- a/src/ClientBuilder.php +++ b/src/ClientBuilder.php @@ -41,7 +41,7 @@ */ final class ClientBuilder { - public const SUPPORTED_SCHEMES = ['', 'bolt', 'bolt+s', 'bolt+ssc', 'neo4j', 'neo4j+s', 'neo4j+ssc', 'http', 'https']; + public const SUPPORTED_SCHEMES = ['', 'bolt', 'bolt+s', 'bolt+ssc', 'neo4j', 'neo4j+s', 'neo4j+ssc']; /** * @psalm-mutation-free diff --git a/src/Contracts/AuthenticateInterface.php b/src/Contracts/AuthenticateInterface.php index 1379e545..a597d72f 100644 --- a/src/Contracts/AuthenticateInterface.php +++ b/src/Contracts/AuthenticateInterface.php @@ -19,16 +19,10 @@ use Bolt\protocol\V5_2; use Bolt\protocol\V5_3; use Bolt\protocol\V5_4; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; interface AuthenticateInterface { - /** - * Authenticates a RequestInterface with the provided configuration Uri and userAgent. - */ - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface; - /** * Authenticates a Bolt connection with the provided configuration Uri and userAgent. * diff --git a/src/Contracts/FormatterInterface.php b/src/Contracts/FormatterInterface.php index ded59d2e..d2198dfe 100644 --- a/src/Contracts/FormatterInterface.php +++ b/src/Contracts/FormatterInterface.php @@ -14,16 +14,10 @@ namespace Laudis\Neo4j\Contracts; use Bolt\Bolt; -use JsonException; use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltResult; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Http\HttpConnection; -use Laudis\Neo4j\Types\CypherList; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use stdClass; /** * A formatter (aka Hydrator) is reponsible for formatting the incoming results of the driver. @@ -81,37 +75,4 @@ interface FormatterInterface * @return ResultFormat */ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection $connection, float $runStart, float $resultAvailableAfter, Statement $statement, BookmarkHolder $holder); - - /** - * Formats the results of the HTTP protocol to the unified format. - * - * @param iterable $statements - * - * @throws JsonException - * - * @return CypherList - * - * @psalm-mutation-free - */ - public function formatHttpResult(ResponseInterface $response, stdClass $body, HttpConnection $connection, float $resultsAvailableAfter, float $resultsConsumedAfter, iterable $statements): CypherList; - - /** - * Decorates a request to make make sure it requests the correct format. - * - * @see https://neo4j.com/docs/http-api/current/actions/result-format/ - * - * @psalm-mutation-free - */ - public function decorateRequest(RequestInterface $request, ConnectionInterface $connection): RequestInterface; - - /** - * Overrides the statement config of the HTTP protocol. - * - * @see https://neo4j.com/docs/http-api/current/actions/result-format/ - * - * @return array{resultDataContents?: list<'GRAPH'|'ROW'|'REST'>, includeStats?:bool} - * - * @psalm-mutation-free - */ - public function statementConfigOverride(ConnectionInterface $connection): array; } diff --git a/src/Databags/DriverConfiguration.php b/src/Databags/DriverConfiguration.php index 18d57613..edbd355d 100644 --- a/src/Databags/DriverConfiguration.php +++ b/src/Databags/DriverConfiguration.php @@ -39,8 +39,6 @@ final class DriverConfiguration public const DEFAULT_POOL_SIZE = 0x2F; public const DEFAULT_CACHE_IMPLEMENTATION = Cache::class; public const DEFAULT_ACQUIRE_CONNECTION_TIMEOUT = 2.0; - /** @var callable():(HttpPsrBindings|null)|HttpPsrBindings|null */ - private $httpPsrBindings; /** @var callable():(CacheInterface|null)|CacheInterface|null */ private $cache; /** @var callable():(SemaphoreFactoryInterface|null)|SemaphoreFactoryInterface|null */ @@ -48,7 +46,6 @@ final class DriverConfiguration private ?Neo4jLogger $logger; /** - * @param callable():(HttpPsrBindings|null)|HttpPsrBindings|null $httpPsrBindings * @param callable():(CacheInterface|null)|CacheInterface|null $cache * @param callable():(SemaphoreFactoryInterface|null)|SemaphoreFactoryInterface|null $semaphore * @param string|null $logLevel The log level to use. If null, LogLevel::INFO is used. @@ -57,7 +54,6 @@ final class DriverConfiguration */ public function __construct( private string|null $userAgent, - callable|HttpPsrBindings|null $httpPsrBindings, private SslConfiguration $sslConfig, private int|null $maxPoolSize, CacheInterface|callable|null $cache, @@ -66,7 +62,6 @@ public function __construct( ?string $logLevel, ?LoggerInterface $logger ) { - $this->httpPsrBindings = $httpPsrBindings; $this->cache = $cache; $this->semaphoreFactory = $semaphore; if ($logger !== null) { @@ -77,13 +72,10 @@ public function __construct( } /** - * @param callable():(HttpPsrBindings|null)|HttpPsrBindings|null $httpPsrBindings - * * @pure */ public static function create( ?string $userAgent, - callable|HttpPsrBindings|null $httpPsrBindings, SslConfiguration $sslConfig, int $maxPoolSize, CacheInterface $cache, @@ -94,7 +86,6 @@ public static function create( ): self { return new self( $userAgent, - $httpPsrBindings, $sslConfig, $maxPoolSize, $cache, @@ -107,7 +98,7 @@ public static function create( /** * Creates a default configuration with a user agent based on the driver version - * and HTTP PSR implementation auto-detected from the environment. + * auto-detected from the environment. * * @pure */ @@ -115,7 +106,6 @@ public static function default(): self { return new self( null, - HttpPsrBindings::default(), SslConfiguration::default(), null, null, @@ -160,21 +150,6 @@ public function withUserAgent($userAgent): self return $tbr; } - /** - * Creates a new configuration with the provided bindings. - * - * @param callable():(HttpPsrBindings|null)|HttpPsrBindings|null $bindings - * - * @psalm-immutable - */ - public function withHttpPsrBindings($bindings): self - { - $tbr = clone $this; - $tbr->httpPsrBindings = $bindings; - - return $tbr; - } - /** * @psalm-immutable */ @@ -194,15 +169,6 @@ public function getSslConfiguration(): SslConfiguration return $this->sslConfig; } - public function getHttpPsrBindings(): HttpPsrBindings - { - $this->httpPsrBindings = (is_callable($this->httpPsrBindings)) ? call_user_func( - $this->httpPsrBindings - ) : $this->httpPsrBindings; - - return $this->httpPsrBindings ??= HttpPsrBindings::default(); - } - public function getMaxPoolSize(): int { return $this->maxPoolSize ?? self::DEFAULT_POOL_SIZE; diff --git a/src/Databags/HttpPsrBindings.php b/src/Databags/HttpPsrBindings.php deleted file mode 100644 index 12b0048b..00000000 --- a/src/Databags/HttpPsrBindings.php +++ /dev/null @@ -1,133 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Databags; - -use function call_user_func; - -use Http\Discovery\Psr17FactoryDiscovery; -use Http\Discovery\Psr18ClientDiscovery; - -use function is_callable; - -use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\RequestFactoryInterface; -use Psr\Http\Message\StreamFactoryInterface; - -/** - * Class containing all relevant implementation of the PSR-18 and PSR-17. - * - * @see https://www.php-fig.org/psr/psr-18/ - * @see https://www.php-fig.org/psr/psr-17/ - * @see https://www.php-fig.org/psr/psr-7/ - */ -final class HttpPsrBindings -{ - /** @var ClientInterface|callable():ClientInterface */ - private $client; - /** @var StreamFactoryInterface|callable():StreamFactoryInterface */ - private $streamFactory; - /** @var RequestFactoryInterface|callable():RequestFactoryInterface */ - private $requestFactory; - - /** - * @psalm-mutation-free - * - * @param callable():ClientInterface|ClientInterface|null $client - * @param callable():StreamFactoryInterface|StreamFactoryInterface|null $streamFactory - * @param callable():RequestFactoryInterface|RequestFactoryInterface|null $requestFactory - */ - public function __construct(callable|ClientInterface|null $client = null, callable|StreamFactoryInterface|null $streamFactory = null, callable|RequestFactoryInterface|null $requestFactory = null) - { - $this->client = $client ?? static fn (): ClientInterface => Psr18ClientDiscovery::find(); - $this->streamFactory = $streamFactory ?? static fn (): StreamFactoryInterface => Psr17FactoryDiscovery::findStreamFactory(); - $this->requestFactory = $requestFactory ?? static fn (): RequestFactoryInterface => Psr17FactoryDiscovery::findRequestFactory(); - } - - /** - * @pure - * - * @param callable():ClientInterface|ClientInterface|null $client - * @param callable():StreamFactoryInterface|StreamFactoryInterface|null $streamFactory - * @param callable():RequestFactoryInterface|RequestFactoryInterface|null $requestFactory - */ - public static function create(callable|ClientInterface|null $client = null, callable|StreamFactoryInterface|null $streamFactory = null, callable|RequestFactoryInterface|null $requestFactory = null): self - { - return new self($client, $streamFactory, $requestFactory); - } - - /** - * @pure - */ - public static function default(): self - { - return new self(); - } - - public function getClient(): ClientInterface - { - if (is_callable($this->client)) { - $this->client = call_user_func($this->client); - } - - return $this->client; - } - - /** - * Creates new bindings with the provided client. - * - * @param ClientInterface|callable():ClientInterface $client - */ - public function withClient(ClientInterface|callable $client): self - { - return new self($client, $this->streamFactory, $this->requestFactory); - } - - /** - * Creates new bindings with the provided stream factory. - * - * @param StreamFactoryInterface|callable():StreamFactoryInterface $factory - */ - public function withStreamFactory(StreamFactoryInterface|callable $factory): self - { - return new self($this->client, $factory, $this->requestFactory); - } - - /** - * Creates new bindings with the request factory. - * - * @param RequestFactoryInterface|callable():RequestFactoryInterface $factory - */ - public function withRequestFactory(RequestFactoryInterface|callable $factory): self - { - return new self($this->client, $this->streamFactory, $factory); - } - - public function getStreamFactory(): StreamFactoryInterface - { - if (is_callable($this->streamFactory)) { - $this->streamFactory = call_user_func($this->streamFactory); - } - - return $this->streamFactory; - } - - public function getRequestFactory(): RequestFactoryInterface - { - if (is_callable($this->requestFactory)) { - $this->requestFactory = call_user_func($this->requestFactory); - } - - return $this->requestFactory; - } -} diff --git a/src/DriverFactory.php b/src/DriverFactory.php index 85d7f3b4..aa48c00e 100644 --- a/src/DriverFactory.php +++ b/src/DriverFactory.php @@ -22,8 +22,8 @@ use Laudis\Neo4j\Contracts\DriverInterface; use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\DriverConfiguration; +use Laudis\Neo4j\Exception\UnsupportedScheme; use Laudis\Neo4j\Formatter\OGMFormatter; -use Laudis\Neo4j\Http\HttpDriver; use Laudis\Neo4j\Neo4j\Neo4jDriver; use Psr\Http\Message\UriInterface; @@ -39,6 +39,8 @@ final class DriverFactory * * @param FormatterInterface $formatter * + * @throws UnsupportedScheme + * * @return ( * func_num_args() is 4 * ? DriverInterface @@ -62,7 +64,7 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co return self::createNeo4jDriver($uri, $configuration, $authenticate, $formatter); } - return self::createHttpDriver($uri, $configuration, $authenticate, $formatter); + throw UnsupportedScheme::make($scheme, ['bolt', 'bolt+s', 'bolt+ssc', 'neo4j', 'neo4j+s', 'neo4j+ssc']); } /** @@ -104,26 +106,4 @@ private static function createNeo4jDriver(string|UriInterface $uri, ?DriverConfi return Neo4jDriver::create($uri, $configuration, $authenticate); } - - /** - * @template U - * - * @param FormatterInterface $formatter - * - * @return ( - * func_num_args() is 4 - * ? DriverInterface - * : DriverInterface - * ) - * - * @pure - */ - private static function createHttpDriver(string|UriInterface $uri, ?DriverConfiguration $configuration, ?AuthenticateInterface $authenticate, ?FormatterInterface $formatter = null): DriverInterface - { - if ($formatter !== null) { - return HttpDriver::create($uri, $configuration, $authenticate, $formatter); - } - - return HttpDriver::create($uri, $configuration, $authenticate); - } } diff --git a/src/Enum/ConnectionProtocol.php b/src/Enum/ConnectionProtocol.php index 2255ed68..e5d68b0e 100644 --- a/src/Enum/ConnectionProtocol.php +++ b/src/Enum/ConnectionProtocol.php @@ -41,7 +41,6 @@ * @method static ConnectionProtocol BOLT_V5_2() * @method static ConnectionProtocol BOLT_V5_3() * @method static ConnectionProtocol BOLT_V5_4() - * @method static ConnectionProtocol HTTP() * * @extends TypedEnum * @@ -62,13 +61,6 @@ final class ConnectionProtocol extends TypedEnum implements JsonSerializable private const BOLT_V5_2 = '5.2'; private const BOLT_V5_3 = '5.3'; private const BOLT_V5_4 = '5.4'; - private const HTTP = 'http'; - - public function isBolt(): bool - { - /** @psalm-suppress ImpureMethodCall */ - return $this !== self::HTTP(); - } /** * @pure diff --git a/src/Formatter/OGMFormatter.php b/src/Formatter/OGMFormatter.php index b2a03e08..6de6b90e 100644 --- a/src/Formatter/OGMFormatter.php +++ b/src/Formatter/OGMFormatter.php @@ -17,14 +17,11 @@ use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltResult; -use Laudis\Neo4j\Contracts\ConnectionInterface; use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\Bookmark; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\Statement; use Laudis\Neo4j\Formatter\Specialised\BoltOGMTranslator; -use Laudis\Neo4j\Formatter\Specialised\JoltHttpOGMTranslator; -use Laudis\Neo4j\Formatter\Specialised\LegacyHttpOGMTranslator; use Laudis\Neo4j\Types\Cartesian3DPoint; use Laudis\Neo4j\Types\CartesianPoint; use Laudis\Neo4j\Types\CypherList; @@ -41,11 +38,6 @@ use Laudis\Neo4j\Types\Time; use Laudis\Neo4j\Types\WGS843DPoint; use Laudis\Neo4j\Types\WGS84Point; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use stdClass; - -use function version_compare; /** * Formats the result in a basic OGM (Object Graph Mapping) format by mapping al cypher types to types found in the \Laudis\Neo4j\Types namespace. @@ -68,8 +60,6 @@ final class OGMFormatter implements FormatterInterface */ public function __construct( private readonly BoltOGMTranslator $boltTranslator, - private readonly JoltHttpOGMTranslator $joltTranslator, - private readonly LegacyHttpOGMTranslator $legacyHttpTranslator ) {} /** @@ -79,7 +69,7 @@ public function __construct( */ public static function create(): OGMFormatter { - return new self(new BoltOGMTranslator(), new JoltHttpOGMTranslator(), new LegacyHttpOGMTranslator()); + return new self(new BoltOGMTranslator()); } /** @@ -105,39 +95,6 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection return $tbr; } - /** - * @psalm-mutation-free - */ - public function formatHttpResult( - ResponseInterface $response, - stdClass $body, - ConnectionInterface $connection, - float $resultsAvailableAfter, - float $resultsConsumedAfter, - iterable $statements - ): CypherList { - return $this->decideTranslator($connection)->formatHttpResult( - $response, - $body, - $connection, - $resultsAvailableAfter, - $resultsConsumedAfter, - $statements - ); - } - - /** - * @psalm-mutation-free - */ - private function decideTranslator(ConnectionInterface $connection): LegacyHttpOGMTranslator|JoltHttpOGMTranslator - { - if (version_compare($connection->getServerAgent(), '4.2.5') <= 0) { - return $this->legacyHttpTranslator; - } - - return $this->joltTranslator; - } - /** * @param BoltMeta $meta * @param list $result @@ -156,20 +113,4 @@ private function formatRow(array $meta, array $result): CypherMap return new CypherMap($map); } - - /** - * @psalm-mutation-free - */ - public function decorateRequest(RequestInterface $request, ConnectionInterface $connection): RequestInterface - { - return $this->decideTranslator($connection)->decorateRequest($request); - } - - /** - * @psalm-mutation-free - */ - public function statementConfigOverride(ConnectionInterface $connection): array - { - return $this->decideTranslator($connection)->statementConfigOverride(); - } } diff --git a/src/Formatter/Specialised/HttpMetaInfo.php b/src/Formatter/Specialised/HttpMetaInfo.php deleted file mode 100644 index fb545796..00000000 --- a/src/Formatter/Specialised/HttpMetaInfo.php +++ /dev/null @@ -1,124 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Formatter\Specialised; - -use function is_array; - -use stdClass; - -/** - * @psalm-immutable - */ -final class HttpMetaInfo -{ - /** - * @param list $meta - * @param list $nodes - * @param list $relationships - */ - public function __construct( - private array $meta, - private array $nodes, - private array $relationships, - private int $currentMeta = 0 - ) {} - - /** - * @pure - */ - public static function createFromData(stdClass $data): self - { - /** @var stdClass */ - $graph = $data->graph; - - /** @psalm-suppress MixedArgument */ - return new self($data->meta, $graph->nodes, $graph->relationships); - } - - /** - * @return stdClass|list|null - */ - public function currentMeta() - { - return $this->meta[$this->currentMeta] ?? null; - } - - public function currentNode(): ?stdClass - { - $meta = $this->currentMeta(); - if ($meta === null || is_array($meta)) { - return null; - } - - foreach ($this->nodes as $node) { - if ((int) $node->id === $meta->id) { - return $node; - } - } - - return null; - } - - public function getCurrentRelationship(): ?stdClass - { - $meta = $this->currentMeta(); - if ($meta === null || is_array($meta)) { - return null; - } - - foreach ($this->relationships as $relationship) { - if ((int) $relationship->id === $meta->id) { - return $relationship; - } - } - - return null; - } - - public function getCurrentType(): ?string - { - $currentMeta = $this->currentMeta(); - if (is_array($currentMeta)) { - return 'path'; - } - - if ($currentMeta === null) { - return null; - } - - /** @var string */ - return $currentMeta->type; - } - - public function withNestedMeta(): self - { - $tbr = clone $this; - - $currentMeta = $this->currentMeta(); - if (is_array($currentMeta)) { - $tbr->meta = $currentMeta; - $tbr->currentMeta = 0; - } - - return $tbr; - } - - public function incrementMeta(): self - { - $tbr = clone $this; - ++$tbr->currentMeta; - - return $tbr; - } -} diff --git a/src/Formatter/Specialised/JoltHttpOGMTranslator.php b/src/Formatter/Specialised/JoltHttpOGMTranslator.php deleted file mode 100644 index dadac1ba..00000000 --- a/src/Formatter/Specialised/JoltHttpOGMTranslator.php +++ /dev/null @@ -1,466 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Formatter\Specialised; - -use Closure; - -use const DATE_ATOM; - -use DateInterval; -use DateTimeImmutable; - -use function is_array; - -use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Contracts\PointInterface; -use Laudis\Neo4j\Formatter\OGMFormatter; -use Laudis\Neo4j\Http\HttpHelper; -use Laudis\Neo4j\Types\Cartesian3DPoint; -use Laudis\Neo4j\Types\CartesianPoint; -use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; -use Laudis\Neo4j\Types\Date; -use Laudis\Neo4j\Types\DateTime; -use Laudis\Neo4j\Types\Duration; -use Laudis\Neo4j\Types\LocalDateTime; -use Laudis\Neo4j\Types\LocalTime; -use Laudis\Neo4j\Types\Node; -use Laudis\Neo4j\Types\Path; -use Laudis\Neo4j\Types\Relationship; -use Laudis\Neo4j\Types\Time; -use Laudis\Neo4j\Types\UnboundRelationship; -use Laudis\Neo4j\Types\WGS843DPoint; -use Laudis\Neo4j\Types\WGS84Point; - -use function preg_match; - -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use RuntimeException; -use stdClass; - -use function str_pad; - -use const STR_PAD_RIGHT; - -use function str_replace; -use function str_starts_with; -use function strtolower; - -use UnexpectedValueException; - -/** - * @psalm-immutable - * - * @psalm-import-type OGMTypes from OGMFormatter - * - * @psalm-suppress PossiblyUndefinedArrayOffset - */ -final class JoltHttpOGMTranslator -{ - /** @var array */ - private array $rawToTypes; - - public function __construct() - { - /** @psalm-suppress InvalidPropertyAssignmentValue */ - $this->rawToTypes = [ - '?' => static fn (string $value): bool => strtolower($value) === 'true', - 'Z' => static fn (string $value): int => (int) $value, - 'R' => static fn (string $value): float => (float) $value, - 'U' => static fn (string $value): string => $value, - 'T' => Closure::fromCallable($this->translateDateTime(...)), - '@' => Closure::fromCallable($this->translatePoint(...)), - '#' => Closure::fromCallable($this->translateBinary(...)), - '[]' => Closure::fromCallable($this->translateList(...)), - '{}' => Closure::fromCallable($this->translateMap(...)), - '()' => Closure::fromCallable($this->translateNode(...)), - '->' => Closure::fromCallable($this->translateRightRelationship(...)), - '<-' => Closure::fromCallable($this->translateLeftRelationship(...)), - '..' => Closure::fromCallable($this->translatePath(...)), - ]; - } - - public function decorateRequest(RequestInterface $request): RequestInterface - { - /** @psalm-suppress ImpureMethodCall */ - return $request->withHeader( - 'Accept', - 'application/vnd.neo4j.jolt+json-seq;strict=true;charset=UTF-8' - ); - } - - /** - * @return array{resultDataContents?: list<'GRAPH'|'ROW'|'REST'>, includeStats?:bool} - */ - public function statementConfigOverride(): array - { - return []; - } - - /** - * @return CypherList>> - */ - public function formatHttpResult( - ResponseInterface $response, - stdClass $body, - ConnectionInterface $connection, - float $resultsAvailableAfter, - float $resultsConsumedAfter, - iterable $statements - ): CypherList { - $allResults = []; - /** @var stdClass $result */ - foreach ($body->results as $result) { - /** @var stdClass $header */ - $header = $result->header; - /** @var list $fields */ - $fields = $header->fields; - $rows = []; - - /** @var list $data */ - foreach ($result->data as $data) { - $row = []; - foreach ($data as $key => $value) { - $row[$fields[$key]] = $this->translateJoltType($value); - } - $rows[] = new CypherMap($row); - } - $allResults[] = new CypherList($rows); - } - - return new CypherList($allResults); - } - - /** - * @return OGMTypes - */ - private function translateJoltType(?stdClass $value) - { - if (is_null($value)) { - return null; - } - - /** @var mixed $input */ - [$key, $input] = HttpHelper::splitJoltSingleton($value); - if (!array_key_exists($key, $this->rawToTypes)) { - throw new UnexpectedValueException('Unexpected Jolt key: '.$key); - } - - return $this->rawToTypes[$key]($input); - } - - /** - * Assumes that 2D points are of the form "SRID=$srid;POINT($x $y)" and 3D points are of the form "SRID=$srid;POINT Z($x $y $z)". - * - * @throws UnexpectedValueException - */ - private function translatePoint(string $value): PointInterface - { - [$srid, $coordinates] = explode(';', $value, 2); - - $srid = $this->getSRID($srid); - $coordinates = $this->getCoordinates($coordinates); - - if ($srid === CartesianPoint::SRID) { - return new CartesianPoint( - (float) $coordinates[0], - (float) $coordinates[1], - ); - } - if ($srid === Cartesian3DPoint::SRID) { - return new Cartesian3DPoint( - (float) $coordinates[0], - (float) $coordinates[1], - (float) ($coordinates[2] ?? 0.0), - ); - } - if ($srid === WGS84Point::SRID) { - return new WGS84Point( - (float) $coordinates[0], - (float) $coordinates[1], - ); - } - if ($srid === WGS843DPoint::SRID) { - return new WGS843DPoint( - (float) $coordinates[0], - (float) $coordinates[1], - (float) ($coordinates[2] ?? 0.0), - ); - } - throw new UnexpectedValueException('A point with srid '.$srid.' has been returned, which has not been implemented.'); - } - - private function getSRID(string $value): int - { - $matches = []; - if (!preg_match('/^SRID=([0-9]+)$/', $value, $matches)) { - throw new UnexpectedValueException('Unexpected SRID string: '.$value); - } - - /** @var array{0: string, 1: string} $matches */ - return (int) $matches[1]; - } - - /** - * @return array{0: string, 1: string, 2?: string} $coordinates - */ - private function getCoordinates(string $value): array - { - $matches = []; - if (!preg_match('/^POINT ?(Z?) ?\(([0-9. ]+)\)$/', $value, $matches)) { - throw new UnexpectedValueException('Unexpected point coordinates string: '.$value); - } - /** @var array{0: string, 1: string, 2: string} $matches */ - $coordinates = explode(' ', $matches[2]); - if ($matches[1] === 'Z' && count($coordinates) !== 3) { - throw new UnexpectedValueException('Expected 3 coordinates in string: '.$value); - } - - if ($matches[1] !== 'Z' && count($coordinates) !== 2) { - throw new UnexpectedValueException('Expected 2 coordinates in string: '.$value); - } - - /** @var array{0: string, 1: string, 2?: string} */ - return $coordinates; - } - - /** - * @return CypherMap - */ - private function translateMap(stdClass $value): CypherMap - { - return new CypherMap( - function () use ($value) { - /** @var stdClass|array|null $element */ - foreach ((array) $value as $key => $element) { - // There is an odd case in the JOLT protocol when dealing with properties in a node. - // Lists appear not to receive a composite type label, - // which is why we have to handle them specifically here. - // @see https://github.com/neo4j/neo4j/issues/12858 - if (is_array($element)) { - yield $key => new CypherList($element); - } else { - yield $key => $this->translateJoltType($element); - } - } - } - ); - } - - private function translateList(array $value): CypherList - { - return new CypherList( - function () use ($value) { - /** @var stdClass|null $element */ - foreach ($value as $element) { - yield $this->translateJoltType($element); - } - } - ); - } - - /** - * @param list $value - */ - private function translatePath(array $value): Path - { - $nodes = []; - /** @var list $relations */ - $relations = []; - $ids = []; - foreach ($value as $nodeOrRelation) { - /** @var Node|Relationship $nodeOrRelation */ - $nodeOrRelation = $this->translateJoltType($nodeOrRelation); - - if ($nodeOrRelation instanceof Relationship) { - $relations[] = $nodeOrRelation; - } else { - $nodes[] = $nodeOrRelation; - } - - $ids[] = $nodeOrRelation->getId(); - } - - return new Path(new CypherList($nodes), new CypherList($relations), new CypherList($ids)); - } - - /** - * @param array{0: int, 1: list, 2: stdClass} $value - */ - private function translateNode(array $value): Node - { - return new Node($value[0], new CypherList($value[1]), $this->translateMap($value[2]), null); - } - - /** - * @param array{0:int, 1: int, 2: string, 3:int, 4: stdClass} $value - */ - private function translateRightRelationship(array $value): Relationship - { - return new Relationship($value[0], $value[1], $value[3], $value[2], $this->translateMap($value[4]), null); - } - - /** - * @param array{0:int, 1: int, 2: string, 3:int, 4: stdClass} $value - */ - private function translateLeftRelationship(array $value): Relationship - { - return new Relationship($value[0], $value[3], $value[1], $value[2], $this->translateMap($value[4]), null); - } - - private function translateBinary(): Closure - { - throw new UnexpectedValueException('Binary data has not been implemented'); - } - - private const TIME_REGEX = '(?\d{2}):(?\d{2}):(?\d{2})((\.)(?\d+))?'; - private const DATE_REGEX = '(?[\-−]?\d+-\d{2}-\d{2})'; - private const ZONE_REGEX = '(?.+)'; - - /** - * @psalm-suppress ImpureMethodCall - * @psalm-suppress ImpureFunctionCall - * @psalm-suppress PossiblyFalseReference - */ - private function translateDateTime(string $datetime): Date|LocalDateTime|LocalTime|DateTime|Duration|Time - { - if (preg_match('/^'.self::DATE_REGEX.'$/u', $datetime, $matches)) { - $days = $this->daysFromMatches($matches); - - return new Date($days); - } - - if (preg_match('/^'.self::TIME_REGEX.'$/u', $datetime, $matches)) { - $nanoseconds = $this->nanosecondsFromMatches($matches); - - return new LocalTime($nanoseconds); - } - - if (preg_match('/^'.self::TIME_REGEX.self::ZONE_REGEX.'$/u', $datetime, $matches)) { - $nanoseconds = $this->nanosecondsFromMatches($matches); - - $offset = $this->offsetFromMatches($matches); - - return new Time($nanoseconds, $offset); - } - - if (preg_match('/^'.self::DATE_REGEX.'T'.self::TIME_REGEX.'$/u', $datetime, $matches)) { - $nanoseconds = $this->nanosecondsFromMatches($matches); - $seconds = $this->secondsInDaysFromMatches($matches); - - [$seconds, $nanoseconds] = $this->addNanoSecondsToSeconds($nanoseconds, $seconds); - - return new LocalDateTime($seconds, $nanoseconds); - } - - if (preg_match('/^'.self::DATE_REGEX.'T'.self::TIME_REGEX.self::ZONE_REGEX.'$/u', $datetime, $matches)) { - $nanoseconds = $this->nanosecondsFromMatches($matches); - $seconds = $this->secondsInDaysFromMatches($matches); - - [$seconds, $nanoseconds] = $this->addNanoSecondsToSeconds($nanoseconds, $seconds); - - $offset = $this->offsetFromMatches($matches); - - return new DateTime($seconds, $nanoseconds, $offset, true); - } - - if (str_starts_with($datetime, 'P')) { - return $this->durationFromFormat($datetime); - } - - throw new UnexpectedValueException(sprintf('Could not handle date/time "%s"', $datetime)); - } - - private function nanosecondsFromMatches(array $matches): int - { - /** @var array{0: string, hours: string, minutes: string, seconds: string, nanoseconds?: string} $matches */ - ['hours' => $hours, 'minutes' => $minutes, 'seconds' => $seconds] = $matches; - $seconds = (((int) $hours) * 60 * 60) + (((int) $minutes) * 60) + ((int) $seconds); - - $nanoseconds = $matches['nanoseconds'] ?? '0'; - $nanoseconds = str_pad($nanoseconds, 9, '0', STR_PAD_RIGHT); - - return $seconds * 1000 * 1000 * 1000 + (int) $nanoseconds; - } - - private function offsetFromMatches(array $matches): int - { - /** @var array{zone: string} $matches */ - $zone = $matches['zone']; - - if (preg_match('/(\d{2}):(\d{2})/', $zone, $matches)) { - /** @var array{0: string, 1: string, 2: string} $matches */ - return ((int) $matches[1]) * 60 * 60 + (int) $matches[2] * 60; - } - - return 0; - } - - private function daysFromMatches(array $matches): int - { - /** @var array{date: string} $matches */ - $date = DateTimeImmutable::createFromFormat('Y-m-d', $matches['date']); - if ($date === false) { - throw new RuntimeException(sprintf('Cannot create DateTime from "%s" in format "Y-m-d"', $matches['date'])); - } - - /** @psalm-suppress ImpureMethodCall */ - return (int) $date->diff(new DateTimeImmutable('@0'))->format('%a'); - } - - private function secondsInDaysFromMatches(array $matches): int - { - /** @var array{date: string} $matches */ - $date = DateTimeImmutable::createFromFormat(DATE_ATOM, $matches['date'].'T00:00:00+00:00'); - if ($date === false) { - throw new RuntimeException(sprintf('Cannot create DateTime from "%s" in format "Y-m-d"', $matches['date'])); - } - - return $date->getTimestamp(); - } - - /** - * @return array{0: int, 1: int} - */ - private function addNanoSecondsToSeconds(int $nanoseconds, int $seconds): array - { - $seconds += (int) ($nanoseconds / 1000 / 1000 / 1000); - $nanoseconds %= 1_000_000_000; - - return [$seconds, $nanoseconds]; - } - - /** - * @psalm-suppress ImpureMethodCall - */ - private function durationFromFormat(string $datetime): Duration - { - $nanoseconds = 0; - // PHP date interval does not understand fractions of a second. - if (preg_match('/\.(?\d+)S/u', $datetime, $matches)) { - /** @var array{0: string, nanoseconds: string} $matches */ - $nanoseconds = (int) str_pad($matches['nanoseconds'], 9, '0', STR_PAD_RIGHT); - - $datetime = str_replace($matches[0], 'S', $datetime); - } - - $interval = new DateInterval($datetime); - $months = (int) $interval->format('%y') * 12 + (int) $interval->format('%m'); - $days = (int) $interval->format('%d'); - $seconds = (int) $interval->format('%h') * 60 * 60 + (int) $interval->format('%i') * 60 + (int) $interval->format('%s'); - - return new Duration($months, $days, $seconds, $nanoseconds); - } -} diff --git a/src/Formatter/Specialised/LegacyHttpOGMTranslator.php b/src/Formatter/Specialised/LegacyHttpOGMTranslator.php deleted file mode 100644 index e66a71eb..00000000 --- a/src/Formatter/Specialised/LegacyHttpOGMTranslator.php +++ /dev/null @@ -1,536 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Formatter\Specialised; - -use function array_combine; -use function array_key_exists; -use function count; -use function date; - -use DateInterval; -use DateTimeImmutable; -use Exception; - -use function explode; -use function is_array; -use function is_object; -use function is_string; -use function json_encode; - -use const JSON_THROW_ON_ERROR; - -use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Contracts\PointInterface; -use Laudis\Neo4j\Formatter\OGMFormatter; -use Laudis\Neo4j\Types\Cartesian3DPoint; -use Laudis\Neo4j\Types\CartesianPoint; -use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; -use Laudis\Neo4j\Types\Date; -use Laudis\Neo4j\Types\DateTime; -use Laudis\Neo4j\Types\Duration; -use Laudis\Neo4j\Types\LocalDateTime; -use Laudis\Neo4j\Types\LocalTime; -use Laudis\Neo4j\Types\Node; -use Laudis\Neo4j\Types\Path; -use Laudis\Neo4j\Types\Relationship; -use Laudis\Neo4j\Types\Time; -use Laudis\Neo4j\Types\UnboundRelationship; -use Laudis\Neo4j\Types\WGS843DPoint; -use Laudis\Neo4j\Types\WGS84Point; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use RuntimeException; - -use function sprintf; - -use stdClass; - -use function str_pad; -use function substr; - -use UnexpectedValueException; - -/** - * @psalm-import-type OGMTypes from OGMFormatter - * - * @psalm-immutable - */ -final class LegacyHttpOGMTranslator -{ - /** - * @psalm-mutation-free - * - * @return CypherList>> - */ - public function formatHttpResult( - ResponseInterface $response, - stdClass $body, - ConnectionInterface $connection, - float $resultsAvailableAfter, - float $resultsConsumedAfter, - iterable $statements - ): CypherList { - /** @var list>> $tbr */ - $tbr = []; - - /** @var list $results */ - $results = $body->results; - foreach ($results as $result) { - $tbr[] = $this->translateResult($result); - } - - return new CypherList($tbr); - } - - public function decorateRequest(RequestInterface $request): RequestInterface - { - return $request; - } - - /** - * @return array{resultDataContents?: list<'GRAPH'|'ROW'|'REST'>, includeStats?:bool} - */ - public function statementConfigOverride(): array - { - return [ - 'resultDataContents' => ['ROW', 'GRAPH'], - ]; - } - - /** - * @throws Exception - * - * @return CypherList> - */ - public function translateResult(stdClass $result): CypherList - { - /** @var list> $tbr */ - $tbr = []; - - /** @var list $columns */ - $columns = $result->columns; - /** @var list $datas */ - $datas = $result->data; - foreach ($datas as $data) { - $meta = HttpMetaInfo::createFromData($data); - - /** @var list $row */ - $row = $data->row; - $row = array_combine($columns, $row); - $tbr[] = $this->translateCypherMap($row, $meta)[0]; - } - - return new CypherList($tbr); - } - - /** - * @param array $row - * - * @return array{0: CypherMap, 1: HttpMetaInfo} - */ - public function translateCypherMap(array $row, HttpMetaInfo $meta): array - { - /** @var array $record */ - $record = []; - foreach ($row as $key => $value) { - [$translation, $meta] = $this->translateValue($value, $meta); - - $record[$key] = $translation; - } - - return [new CypherMap($record), $meta]; - } - - /** - * @param array|scalar|stdClass|null $value - * - * @return array{0: OGMTypes, 1: HttpMetaInfo} - * - * @psalm-suppress MixedArgumentTypeCoercion - * @psalm-suppress MixedArgument - * @psalm-suppress MixedAssignment - * @psalm-suppress InvalidReturnStatement - * @psalm-suppress ArgumentTypeCoercion - * @psalm-suppress InvalidReturnType - */ - private function translateValue(float|array|bool|int|string|stdClass|null $value, HttpMetaInfo $meta): array - { - if (is_object($value)) { - return $this->translateObject($value, $meta); - } - - if (is_array($value)) { - if ($meta->getCurrentType() === 'path') { - /** - * There are edge cases where multiple paths are wrapped in a list. - * - * @see OGMFormatterIntegrationTest::testPathMultiple for an example - */ - if (array_key_exists(0, $value) && is_array($value[0])) { - $tbr = []; - foreach ($value as $path) { - $tbr[] = $this->path($path, $meta->withNestedMeta()); - $meta = $meta->incrementMeta(); - } - - return [new CypherList($tbr), $meta]; - } - - $tbr = $this->path($value, $meta->withNestedMeta()); - $meta = $meta->incrementMeta(); - - return [$tbr, $meta]; - } - - return $this->translateCypherList($value, $meta); - } - - if (is_string($value)) { - return $this->translateString($value, $meta); - } - - return [$value, $meta->incrementMeta()]; - } - - /** - * @return array{0: Cartesian3DPoint|CartesianPoint|CypherList|CypherMap|Node|Relationship|WGS843DPoint|WGS84Point|Path, 1: HttpMetaInfo} - * - * @psalm-suppress MixedArgument - * @psalm-suppress MixedArgumentTypeCoercion - */ - private function translateObject(stdClass $value, HttpMetaInfo $meta): array - { - $type = $meta->getCurrentType(); - if ($type === 'relationship') { - /** @var stdClass $relationship */ - $relationship = $meta->getCurrentRelationship(); - - return $this->relationship($relationship, $meta); - } - - if ($type === 'point') { - return [$this->translatePoint($value), $meta]; - } - - if ($type === 'node') { - $node = $meta->currentNode(); - if ($node && json_encode($value, JSON_THROW_ON_ERROR) === json_encode($node->properties, JSON_THROW_ON_ERROR)) { - $meta = $meta->incrementMeta(); - $map = $this->translateProperties((array) $node->properties); - - return [new Node((int) $node->id, new CypherList($node->labels), $map, null), $meta]; - } - } - - return $this->translateCypherMap((array) $value, $meta); - } - - /** - * @param array $properties - * - * @return CypherMap - */ - private function translateProperties(array $properties): CypherMap - { - $tbr = []; - foreach ($properties as $key => $value) { - if ($value instanceof stdClass) { - /** @var array $castedValue */ - $castedValue = (array) $value; - $tbr[$key] = $this->translateProperties($castedValue); - } elseif (is_array($value)) { - /** @var array $value */ - $tbr[$key] = new CypherList($this->translateProperties($value)); - } else { - $tbr[$key] = $value; - } - } - /** @var CypherMap */ - return new CypherMap($tbr); - } - - /** - * @psalm-suppress MixedArgument - * @psalm-suppress MixedArgumentTypeCoercion - * - * @return array{0: Relationship, 1: HttpMetaInfo} - */ - private function relationship(stdClass $relationship, HttpMetaInfo $meta): array - { - $meta = $meta->incrementMeta(); - $map = $this->translateProperties((array) $relationship->properties); - - $tbr = new Relationship( - (int) $relationship->id, - (int) $relationship->startNode, - (int) $relationship->endNode, - $relationship->type, - $map, - null - ); - - return [$tbr, $meta]; - } - - /** - * @param list $value - * - * @return array{0: CypherList, 1: HttpMetaInfo} - */ - private function translateCypherList(array $value, HttpMetaInfo $meta): array - { - /** @var array $tbr */ - $tbr = []; - foreach ($value as $x) { - [$x, $meta] = $this->translateValue($x, $meta); - $tbr[] = $x; - } - - return [new CypherList($tbr), $meta]; - } - - /** - * @param list $value - */ - private function path(array $value, HttpMetaInfo $meta): Path - { - /** @var list $nodes */ - $nodes = []; - /** @var list $ids */ - $ids = []; - /** @var list $rels */ - $rels = []; - - foreach ($value as $x) { - /** @var stdClass $currentMeta */ - $currentMeta = $meta->currentMeta(); - /** @var int $id */ - $id = $currentMeta->id; - $ids[] = $id; - [$x, $meta] = $this->translateObject($x, $meta); - if ($x instanceof Node) { - $nodes[] = $x; - } elseif ($x instanceof Relationship) { - $rels[] = new UnboundRelationship($x->getId(), $x->getType(), $x->getProperties(), null); - } - } - - return new Path(new CypherList($nodes), new CypherList($rels), new CypherList($ids)); - } - - /** - * @return CartesianPoint|Cartesian3DPoint|WGS843DPoint|WGS84Point - */ - private function translatePoint(stdClass $value): PointInterface - { - /** @var stdClass $crs */ - $crs = $value->crs; - /** @var array{0: float, 1: float, 2:float} $coordinates */ - $coordinates = $value->coordinates; - /** @var int $srid */ - $srid = $crs->srid; - if ($srid === CartesianPoint::SRID) { - return new CartesianPoint( - $coordinates[0], - $coordinates[1], - ); - } - if ($srid === Cartesian3DPoint::SRID) { - return new Cartesian3DPoint( - $coordinates[0], - $coordinates[1], - $coordinates[2], - ); - } - if ($srid === WGS84Point::SRID) { - return new WGS84Point( - $coordinates[0], - $coordinates[1], - ); - } - if ($srid === WGS843DPoint::SRID) { - return new WGS843DPoint( - $coordinates[0], - $coordinates[1], - $coordinates[2], - ); - } - /** @var string $name */ - $name = $crs->name; - throw new UnexpectedValueException('A point with srid '.$srid.' and name '.$name.' has been returned, which has not been implemented.'); - } - - /** - * @throws Exception - * - * @return array{0: string|Date|DateTime|Duration|LocalDateTime|LocalTime|Time, 1: HttpMetaInfo} - */ - public function translateString(string $value, HttpMetaInfo $meta): array - { - switch ($meta->getCurrentType()) { - case 'duration': - $meta = $meta->incrementMeta(); - $tbr = [$this->translateDuration($value), $meta]; - break; - case 'datetime': - $meta = $meta->incrementMeta(); - $tbr = [$this->translateDateTime($value), $meta]; - break; - case 'date': - $meta = $meta->incrementMeta(); - $tbr = [$this->translateDate($value), $meta]; - break; - case 'time': - $meta = $meta->incrementMeta(); - $tbr = [$this->translateTime($value), $meta]; - break; - case 'localdatetime': - $meta = $meta->incrementMeta(); - $tbr = [$this->translateLocalDateTime($value), $meta]; - break; - case 'localtime': - $meta = $meta->incrementMeta(); - $tbr = [$this->translateLocalTime($value), $meta]; - break; - default: - $tbr = [$value, $meta->incrementMeta()]; - break; - } - - return $tbr; - } - - /** - * @throws Exception - */ - private function translateDuration(string $value): Duration - { - /** @psalm-suppress ImpureFunctionCall false positive in version php 7.4 */ - if (str_contains($value, '.')) { - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$format, $secondsFraction] = explode('.', $value); - $nanoseconds = (int) substr($secondsFraction, 6); - $microseconds = (int) str_pad((string) ((int) substr($secondsFraction, 0, 6)), 6, '0'); - $interval = new DateInterval($format.'S'); - $x = new DateTimeImmutable(); - /** @psalm-suppress PossiblyFalseReference */ - $interval = $x->add($interval)->modify('+'.$microseconds.' microseconds')->diff($x); - } else { - $nanoseconds = 0; - $interval = new DateInterval($value); - } - - $months = $interval->y * 12 + $interval->m; - $days = $interval->d; - $seconds = $interval->h * 60 * 60 + $interval->i * 60 + $interval->s; - $nanoseconds = (int) ($interval->f * 1_000_000_000) + $nanoseconds; - - return new Duration($months, $days, $seconds, $nanoseconds); - } - - private function translateDate(string $value): Date - { - $epoch = new DateTimeImmutable('@0'); - $dateTime = DateTimeImmutable::createFromFormat('Y-m-d', $value); - if ($dateTime === false) { - throw new RuntimeException(sprintf('Could not create date from format "Y-m-d" and %s', $value)); - } - - $diff = $dateTime->diff($epoch); - - /** @psalm-suppress ImpureMethodCall */ - return new Date((int) $diff->format('%a')); - } - - private function translateTime(string $value): Time - { - $value = substr($value, 0, 5); - $values = explode(':', $value); - - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - return new Time((((int) $values[0]) * 60 * 60 + ((int) $values[1]) * 60) * 1_000_000_000, 0); - } - - /** - * @throws Exception - */ - private function translateDateTime(string $value): DateTime - { - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$date, $time] = explode('T', $value); - $tz = null; - if (str_contains($time, '+')) { - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$time, $timezone] = explode('+', $time); - - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$tzHours, $tzMinutes] = explode(':', $timezone); - $tz = (int) $tzHours * 60 * 60 + (int) $tzMinutes * 60; - } - - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$time, $milliseconds] = explode('.', $time); - - $dateTime = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date.' '.$time); - if ($dateTime === false) { - throw new RuntimeException(sprintf('Could not create date from format "Y-m-d H:i:s" and %s', $date.' '.$time)); - } - - if ($tz !== null) { - return new DateTime($dateTime->getTimestamp(), (int) $milliseconds * 1_000_000, $tz, true); - } - - return new DateTime($dateTime->getTimestamp(), (int) $milliseconds * 1_000_000, 0, true); - } - - private function translateLocalDateTime(string $value): LocalDateTime - { - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$date, $time] = explode('T', $value); - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$time, $milliseconds] = explode('.', $time); - - $dateTime = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date.' '.$time); - if ($dateTime === false) { - throw new RuntimeException(sprintf('Could not create date from format "Y-m-d H:i:s" and %s', $date.' '.$time)); - } - - return new LocalDateTime($dateTime->getTimestamp(), (int) $milliseconds * 1_000_000); - } - - /** - * @psalm-suppress all - * - * @throws Exception - */ - private function translateLocalTime(string $value): LocalTime - { - $timestamp = (new DateTimeImmutable($value))->getTimestamp(); - - $hours = (int) date('H', $timestamp); - $minutes = (int) date('i', $timestamp); - $seconds = (int) date('s', $timestamp); - $milliseconds = 0; - - $values = explode('.', $value); - if (count($values) > 1) { - $milliseconds = $values[1]; - } - - $totalSeconds = ($hours * 3600) + ($minutes * 60) + $seconds + ($milliseconds / 1000); - - return new LocalTime((int) $totalSeconds * 1_000_000_000); - } -} diff --git a/src/Formatter/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php index c8864f76..27b29842 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -18,7 +18,6 @@ use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltResult; -use Laudis\Neo4j\Contracts\ConnectionInterface; use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\DatabaseInfo; @@ -28,17 +27,11 @@ use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\SummaryCounters; use Laudis\Neo4j\Enum\QueryTypeEnum; -use Laudis\Neo4j\Http\HttpConnection; use Laudis\Neo4j\Types\CypherList; use Laudis\Neo4j\Types\CypherMap; use function microtime; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use stdClass; -use UnexpectedValueException; - /** * Decorates the result of the provided format with an extensive summary. * @@ -67,67 +60,6 @@ public function __construct( private readonly OGMFormatter $formatter ) {} - /** - * @param CypherList> $results - * - * @return SummarizedResult> - * - * @psalm-mutation-free - */ - public function formatHttpStats(stdClass $response, HttpConnection $connection, Statement $statement, float $resultAvailableAfter, float $resultConsumedAfter, CypherList $results): SummarizedResult - { - if (isset($response->summary) && $response->summary instanceof stdClass) { - /** @var stdClass $stats */ - $stats = $response->summary->stats; - } elseif (isset($response->stats)) { - /** @var stdClass $stats */ - $stats = $response->stats; - } else { - throw new UnexpectedValueException('No stats found in the response set'); - } - - /** - * @psalm-suppress MixedPropertyFetch - * @psalm-suppress MixedArgument - */ - $counters = new SummaryCounters( - $stats->nodes_created ?? 0, - $stats->nodes_deleted ?? 0, - $stats->relationships_created ?? 0, - $stats->relationships_deleted ?? 0, - $stats->properties_set ?? 0, - $stats->labels_added ?? 0, - $stats->labels_removed ?? 0, - $stats->indexes_added ?? 0, - $stats->indexes_removed ?? 0, - $stats->constraints_added ?? 0, - $stats->constraints_removed ?? 0, - $stats->contains_updates ?? false, - $stats->contains_system_updates ?? false, - $stats->system_updates ?? 0, - ); - - $summary = new ResultSummary( - $counters, - $connection->getDatabaseInfo(), - new CypherList(), - null, - null, - $statement, - QueryTypeEnum::fromCounters($counters), - $resultAvailableAfter, - $resultConsumedAfter, - new ServerInfo( - $connection->getServerAddress(), - $connection->getProtocol(), - $connection->getServerAgent() - ) - ); - - /** @var SummarizedResult> */ - return new SummarizedResult($summary, $results); - } - /** * @param array{stats?: BoltCypherStats}&array $response * @@ -201,45 +133,4 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection */ return (new SummarizedResult($summary, $formattedResult))->withCacheLimit($result->getFetchSize()); } - - /** - * @psalm-mutation-free - * - * @psalm-suppress ImpureMethodCall - */ - public function formatHttpResult(ResponseInterface $response, stdClass $body, HttpConnection $connection, float $resultsAvailableAfter, float $resultsConsumedAfter, iterable $statements): CypherList - { - /** @var list>> */ - $tbr = []; - - $toDecorate = $this->formatter->formatHttpResult($response, $body, $connection, $resultsAvailableAfter, $resultsConsumedAfter, $statements); - $i = 0; - foreach ($statements as $statement) { - /** @var list $results */ - $results = $body->results; - $result = $results[$i]; - $tbr[] = $this->formatHttpStats($result, $connection, $statement, $resultsAvailableAfter, $resultsConsumedAfter, $toDecorate->get($i)); - ++$i; - } - - return new CypherList($tbr); - } - - /** - * @psalm-mutation-free - */ - public function decorateRequest(RequestInterface $request, ConnectionInterface $connection): RequestInterface - { - return $this->formatter->decorateRequest($request, $connection); - } - - /** - * @psalm-mutation-free - */ - public function statementConfigOverride(ConnectionInterface $connection): array - { - return array_merge($this->formatter->statementConfigOverride($connection), [ - 'includeStats' => true, - ]); - } } diff --git a/src/Http/HttpConnection.php b/src/Http/HttpConnection.php deleted file mode 100644 index a7a0c8f3..00000000 --- a/src/Http/HttpConnection.php +++ /dev/null @@ -1,165 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use Laudis\Neo4j\Common\ConnectionConfiguration; -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Databags\DatabaseInfo; -use Laudis\Neo4j\Enum\AccessMode; -use Laudis\Neo4j\Enum\ConnectionProtocol; -use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\UriInterface; - -/** - * @implements ConnectionInterface - */ -final class HttpConnection implements ConnectionInterface -{ - private bool $isOpen = true; - - /** - * @psalm-mutation-free - */ - public function __construct( - /** @psalm-readonly */ - private readonly ClientInterface $client, - /** @psalm-readonly */ - private readonly ConnectionConfiguration $config, - private readonly AuthenticateInterface $authenticate, - private readonly string $userAgent - ) {} - - /** - * @psalm-mutation-free - */ - public function getImplementation(): ClientInterface - { - return $this->client; - } - - /** - * @psalm-mutation-free - */ - public function getServerAgent(): string - { - return $this->config->getServerAgent(); - } - - /** - * @psalm-mutation-free - */ - public function getServerAddress(): UriInterface - { - return $this->config->getServerAddress(); - } - - /** - * @psalm-mutation-free - */ - public function getServerVersion(): string - { - return $this->config->getServerVersion(); - } - - /** - * @psalm-mutation-free - */ - public function getProtocol(): ConnectionProtocol - { - return $this->config->getProtocol(); - } - - /** - * @psalm-mutation-free - */ - public function getAccessMode(): AccessMode - { - return $this->config->getAccessMode(); - } - - /** - * @psalm-mutation-free - */ - public function getDatabaseInfo(): DatabaseInfo - { - return $this->config->getDatabaseInfo() ?? new DatabaseInfo(''); - } - - /** - * @psalm-mutation-free - */ - public function isOpen(): bool - { - return $this->isOpen; - } - - /** - * @psalm-external-mutation-free - */ - public function open(): void - { - $this->isOpen = true; - } - - /** - * @psalm-external-mutation-free - */ - public function close(): void - { - $this->isOpen = false; - } - - public function reset(): void - { - // Cannot reset a stateless protocol - } - - public function setTimeout(float $timeout): void - { - // Impossible to actually set a timeout with PSR definition - } - - /** - * @psalm-immutable - */ - public function getAuthentication(): AuthenticateInterface - { - return $this->authenticate; - } - - /** - * @psalm-mutation-free - */ - public function getServerState(): string - { - return 'UNKNOWN'; - } - - /** - * @psalm-mutation-free - */ - public function getEncryptionLevel(): string - { - return $this->config->getEncryptionLevel(); - } - - /** - * @psalm-mutation-free - */ - public function getUserAgent(): string - { - return $this->userAgent; - } -} diff --git a/src/Http/HttpConnectionPool.php b/src/Http/HttpConnectionPool.php deleted file mode 100644 index 2c0e3929..00000000 --- a/src/Http/HttpConnectionPool.php +++ /dev/null @@ -1,135 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use Generator; - -use function json_encode; - -use Laudis\Neo4j\Common\ConnectionConfiguration; -use Laudis\Neo4j\Common\Resolvable; -use Laudis\Neo4j\Common\Uri; -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Contracts\ConnectionPoolInterface; -use Laudis\Neo4j\Databags\DatabaseInfo; -use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Enum\ConnectionProtocol; -use Laudis\Neo4j\Formatter\BasicFormatter; -use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\StreamFactoryInterface; -use Psr\Http\Message\UriInterface; -use Throwable; - -/** - * @implements ConnectionPoolInterface - */ -final class HttpConnectionPool implements ConnectionPoolInterface -{ - /** - * @param Resolvable $client - * @param Resolvable $requestFactory - * @param Resolvable $streamFactory - * @param Resolvable $tsxUrl - * - * @psalm-mutation-free - */ - public function __construct( - /** - * @psalm-readonly - */ - private readonly Resolvable $client, - /** - * @psalm-readonly - */ - private readonly Resolvable $requestFactory, - /** - * @psalm-readonly - */ - private readonly Resolvable $streamFactory, - private readonly AuthenticateInterface $auth, - private readonly string $userAgent, - private readonly Resolvable $tsxUrl - ) {} - - public function acquire(SessionConfiguration $config): Generator - { - yield 0.0; - - $uri = Uri::create($this->tsxUrl->resolve()); - $request = $this->requestFactory->resolve()->createRequest('POST', $uri); - - $path = $request->getUri()->getPath().'/commit'; - $uri = $uri->withPath($path); - $request = $request->withUri($uri); - - $body = json_encode([ - 'statements' => [ - [ - 'statement' => <<<'CYPHER' -CALL dbms.components() -YIELD name, versions, edition -RETURN name, versions, edition -CYPHER - , - ], - ], - 'resultDataContents' => [], - 'includeStats' => false, - ], JSON_THROW_ON_ERROR); - - $request = $request->withBody($this->streamFactory->resolve()->createStream($body)); - - $response = $this->client->resolve()->sendRequest($request); - $data = HttpHelper::interpretResponse($response); - /** @var array{0: array{name: string, versions: list, edition: string}} $results */ - $results = (new BasicFormatter())->formatHttpResult($response, $data, null)->first(); - - $version = $results[0]['versions'][0] ?? ''; - - $config = new ConnectionConfiguration( - $results[0]['name'].'-'.$results[0]['edition'].'/'.$version, - $uri, - $version, - ConnectionProtocol::HTTP(), - $config->getAccessMode(), - new DatabaseInfo($config->getDatabase() ?? ''), - '' - ); - - return new HttpConnection($this->client->resolve(), $config, $this->auth, $this->userAgent); - } - - public function canConnect(UriInterface $uri, AuthenticateInterface $authenticate, ?string $userAgent = null): bool - { - $request = $this->requestFactory->resolve()->createRequest('GET', $uri); - $client = $this->client->resolve(); - - try { - return $client->sendRequest($request)->getStatusCode() === 200; - } catch (Throwable) { - return false; - } - } - - public function release(ConnectionInterface $connection): void - { - // Nothing to release in the current HTTP Protocol implementation - } - - public function close(): void - { - // Nothing to close in the current HTTP Protocol implementation - } -} diff --git a/src/Http/HttpDriver.php b/src/Http/HttpDriver.php deleted file mode 100644 index d798d347..00000000 --- a/src/Http/HttpDriver.php +++ /dev/null @@ -1,206 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use function is_string; - -use Laudis\Neo4j\Authentication\Authenticate; -use Laudis\Neo4j\Common\Resolvable; -use Laudis\Neo4j\Common\Uri; -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Contracts\DriverInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; -use Laudis\Neo4j\Contracts\SessionInterface; -use Laudis\Neo4j\Databags\DriverConfiguration; -use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Formatter\OGMFormatter; -use Psr\Http\Message\StreamFactoryInterface; -use Psr\Http\Message\UriInterface; - -use function str_replace; -use function uniqid; - -/** - * @template T - * - * @implements DriverInterface - * - * @psalm-import-type OGMResults from OGMFormatter - */ -final class HttpDriver implements DriverInterface -{ - private readonly string $key; - - /** - * @psalm-mutation-free - * - * @param FormatterInterface $formatter - */ - public function __construct( - private readonly UriInterface $uri, - private readonly DriverConfiguration $config, - private readonly FormatterInterface $formatter, - private readonly AuthenticateInterface $auth - ) { - /** @psalm-suppress ImpureFunctionCall */ - $this->key = uniqid(); - } - - /** - * @template U - * - * @param FormatterInterface $formatter - * - * @return ( - * func_num_args() is 4 - * ? self - * : self - * ) - * - * @pure - */ - public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null, ?FormatterInterface $formatter = null): self - { - if (is_string($uri)) { - $uri = Uri::create($uri); - } - - $configuration ??= DriverConfiguration::default(); - if ($formatter !== null) { - return new self( - $uri, - $configuration, - $formatter, - $authenticate ?? Authenticate::fromUrl($uri, $configuration->getLogger()) - ); - } - - return new self( - $uri, - $configuration, - OGMFormatter::create(), - $authenticate ?? Authenticate::fromUrl($uri, $configuration->getLogger()) - ); - } - - /** - * @psalm-external-mutation-free - */ - public function createSession(?SessionConfiguration $config = null): SessionInterface - { - $factory = $this->resolvableFactory(); - $config ??= SessionConfiguration::default(); - $config = $config->merge(SessionConfiguration::fromUri($this->uri, null)); - $streamFactoryResolve = $this->streamFactory(); - - $tsxUrl = $this->tsxUrl($config); - - return new HttpSession( - $streamFactoryResolve, - $this->getHttpConnectionPool($tsxUrl), - $config, - $this->formatter, - $factory, - $tsxUrl, - $this->auth, - $this->config->getUserAgent() - ); - } - - public function verifyConnectivity(?SessionConfiguration $config = null): bool - { - $config ??= SessionConfiguration::default(); - - return $this->getHttpConnectionPool($this->tsxUrl($config)) - ->canConnect($this->uri, $this->auth); - } - - /** - * @param Resolvable $tsxUrl - * - * @psalm-mutation-free - */ - private function getHttpConnectionPool(Resolvable $tsxUrl): HttpConnectionPool - { - return new HttpConnectionPool( - Resolvable::once($this->key.':client', fn () => $this->config->getHttpPsrBindings()->getClient()), - $this->resolvableFactory(), - $this->streamFactory(), - $this->auth, - $this->config->getUserAgent(), - $tsxUrl - ); - } - - /** - * @return Resolvable - * - * @psalm-mutation-free - */ - private function resolvableFactory(): Resolvable - { - return Resolvable::once($this->key.':requestFactory', function () { - $bindings = $this->config->getHttpPsrBindings(); - - return new RequestFactory($bindings->getRequestFactory(), $this->auth, $this->uri, $this->config->getUserAgent()); - }); - } - - /** - * @return Resolvable - * - * @psalm-mutation-free - */ - private function streamFactory(): Resolvable - { - return Resolvable::once($this->key.':streamFactory', fn () => $this->config->getHttpPsrBindings()->getStreamFactory()); - } - - /** - * @return Resolvable - * - * @psalm-mutation-free - */ - private function tsxUrl(SessionConfiguration $config): Resolvable - { - return Resolvable::once($this->key.':tsxUrl', function () use ($config) { - $database = $config->getDatabase() ?? 'neo4j'; - $request = $this->resolvableFactory()->resolve()->createRequest('GET', $this->uri); - $client = $this->config->getHttpPsrBindings()->getClient(); - - $response = $client->sendRequest($request); - - $discovery = HttpHelper::interpretResponse($response); - /** @var string|null */ - $version = $discovery->neo4j_version ?? null; - - if ($version === null) { - /** @var string */ - $uri = $discovery->data; - $request = $request->withUri(Uri::create($uri)); - $discovery = HttpHelper::interpretResponse($client->sendRequest($request)); - } - - /** @var string */ - $tsx = $discovery->transaction; - - return str_replace('{databaseName}', $database, $tsx); - }); - } - - public function closeConnections(): void - { - // Nothing to close in the current HTTP Protocol implementation - } -} diff --git a/src/Http/HttpHelper.php b/src/Http/HttpHelper.php deleted file mode 100644 index 822e6880..00000000 --- a/src/Http/HttpHelper.php +++ /dev/null @@ -1,220 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use function array_key_first; -use function array_merge; -use function count; -use function json_decode; -use function json_encode; - -use const JSON_THROW_ON_ERROR; - -use JsonException; -use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; -use Laudis\Neo4j\Databags\Neo4jError; -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Exception\Neo4jException; -use Laudis\Neo4j\ParameterHelper; -use Psr\Http\Message\ResponseInterface; -use RuntimeException; -use stdClass; -use UnexpectedValueException; - -/** - * Helper functions for the http protocol. - * - * @psalm-import-type CypherResponseSet from \Laudis\Neo4j\Contracts\FormatterInterface - */ -final class HttpHelper -{ - /** - * Checks the response and interprets it. Throws if an error is detected. - * - * @throws JsonException - * @throws RuntimeException - * @throws UnexpectedValueException - */ - public static function interpretResponse(ResponseInterface $response): stdClass - { - if ($response->getStatusCode() >= 500) { - throw new RuntimeException('HTTP Error: '.$response->getReasonPhrase()); - } - - $contents = $response->getBody()->getContents(); - - /** @var stdClass $body */ - // Jolt is a Json sequence (rfc 7464), so it starts with a RS control character "\036" - if ($contents[0] === "\036") { - $body = self::getJoltBody($contents); - } else { - // If not Jolt, assume it is Json - $body = self::getJsonBody($contents); - } - - $errors = []; - /** @var list $bodyErrors */ - $bodyErrors = $body->errors ?? []; - foreach ($bodyErrors as $error) { - /** @var string */ - $code = $error->code; - /** @var string */ - $message = $error->message; - $errors[] = Neo4jError::fromMessageAndCode($code, $message); - } - - if (count($errors) !== 0) { - throw new Neo4jException($errors); - } - - return $body; - } - - /** - * @throws JsonException - */ - public static function getJsonBody(string $contents): stdClass - { - /** @var stdClass */ - return json_decode($contents, false, 512, JSON_THROW_ON_ERROR); - } - - /** - * Converts a Jolt input (with JSON sequence separators) into a stdClass that contains the data of all jsons of the sequence. - * - * @throws JsonException - * @throws RuntimeException - * @throws UnexpectedValueException - * - * @psalm-suppress MixedAssignment - * @psalm-suppress MixedArrayAssignment - * @psalm-suppress MixedPropertyFetch - * @psalm-suppress MixedArgument - */ - public static function getJoltBody(string $contents): stdClass - { - // Split json sequence in single jsons, split on json sequence separators. - $contents = explode("\036", $contents); - - // Drop first (empty) string. - array_shift($contents); - - // stdClass to capture all the jsons - $rtr = new stdClass(); - $rtr->results = []; - - // stdClass to capture the jsons of the results of a single statement that has been sent. - $data = new stdClass(); - $data->data = []; - - foreach ($contents as $content) { - $content = self::getJsonBody($content); - [$key, $value] = self::splitJoltSingleton($content); - - switch ($key) { - case 'header': - if (isset($data->header)) { - throw new UnexpectedValueException('Jolt response with second header before summary received'); - } - $data->header = $value; - break; - case 'data': - if (!isset($data->header)) { - throw new UnexpectedValueException('Jolt response with data before new header received'); - } - $data->data[] = $value; - break; - case 'summary': - if (!isset($data->header)) { - throw new UnexpectedValueException('Jolt response with summary before new header received'); - } - $data->summary = $value; - $rtr->results[] = $data; - $data = new stdClass(); - $data->data = []; - break; - - case 'info': - if (isset($rtr->info)) { - throw new UnexpectedValueException('Jolt response with multiple info rows received'); - } - $rtr->info = $value; - break; - case 'error': - if (isset($rtr->errors)) { - throw new UnexpectedValueException('Jolt response with multiple error rows received'); - } - $rtr->errors = []; - foreach ($value->errors as $error) { - $rtr->errors[] = (object) [ - 'code' => self::splitJoltSingleton($error->code)[1], - 'message' => self::splitJoltSingleton($error->message)[1], - ]; - } - break; - default: - throw new UnexpectedValueException('Jolt response with unknown key received: '.$key); - } - } - - return $rtr; - } - - /** - * @pure - * - * @return array{0: string, 1: mixed} - */ - public static function splitJoltSingleton(stdClass $joltSingleton): array - { - /** @var array $joltSingleton */ - $joltSingleton = (array) $joltSingleton; - - if (count($joltSingleton) !== 1) { - throw new UnexpectedValueException('stdClass with '.count($joltSingleton).' elements is not a Jolt singleton.'); - } - - $key = array_key_first($joltSingleton); - - return [$key, $joltSingleton[$key]]; - } - - /** - * Prepares the statements to json. - * - * @param iterable $statements - * - * @throws JsonException - */ - public static function statementsToJson(ConnectionInterface $connection, FormatterInterface $formatter, iterable $statements): string - { - $tbr = []; - foreach ($statements as $statement) { - $st = [ - 'statement' => $statement->getText(), - 'resultDataContents' => [], - 'includeStats' => false, - ]; - $st = array_merge($st, $formatter->statementConfigOverride($connection)); - $parameters = ParameterHelper::formatParameters($statement->getParameters(), $connection->getProtocol()); - $st['parameters'] = $parameters->count() === 0 ? new stdClass() : $parameters->toArray(); - $tbr[] = $st; - } - - return json_encode([ - 'statements' => $tbr, - ], JSON_THROW_ON_ERROR); - } -} diff --git a/src/Http/HttpSession.php b/src/Http/HttpSession.php deleted file mode 100644 index 280e9b84..00000000 --- a/src/Http/HttpSession.php +++ /dev/null @@ -1,191 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use JsonException; -use Laudis\Neo4j\Common\GeneratorHelper; -use Laudis\Neo4j\Common\Resolvable; -use Laudis\Neo4j\Common\TransactionHelper; -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; -use Laudis\Neo4j\Contracts\SessionInterface; -use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; -use Laudis\Neo4j\Databags\Bookmark; -use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Databags\TransactionConfiguration; -use Laudis\Neo4j\Enum\AccessMode; -use Laudis\Neo4j\Types\CypherList; - -use function microtime; -use function parse_url; - -use const PHP_URL_PATH; - -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\StreamFactoryInterface; -use stdClass; - -/** - * @template T - * - * @implements SessionInterface - */ -final class HttpSession implements SessionInterface -{ - /** - * @psalm-mutation-free - * - * @param Resolvable $streamFactory - * @param FormatterInterface $formatter - * @param Resolvable $requestFactory - * @param Resolvable $uri - */ - public function __construct( - /** - * @psalm-readonly - */ - private readonly Resolvable $streamFactory, - /** @psalm-readonly */ - private readonly HttpConnectionPool $pool, - /** @psalm-readonly */ - private readonly SessionConfiguration $config, - /** - * @psalm-readonly - */ - private readonly FormatterInterface $formatter, - /** - * @psalm-readonly - */ - private readonly Resolvable $requestFactory, - /** - * @psalm-readonly - */ - private readonly Resolvable $uri, - AuthenticateInterface $auth, - string $userAgent - ) {} - - /** - * @throws JsonException - */ - public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList - { - $request = $this->requestFactory->resolve()->createRequest('POST', $this->uri->resolve()); - $connection = $this->pool->acquire($this->config); - /** @var HttpConnection */ - $connection = GeneratorHelper::getReturnFromGenerator($connection); - $content = HttpHelper::statementsToJson($connection, $this->formatter, $statements); - $request = $this->formatter->decorateRequest($request, $connection); - $request = $this->instantCommitRequest($request)->withBody($this->streamFactory->resolve()->createStream($content)); - - $start = microtime(true); - $response = $connection->getImplementation()->sendRequest($request); - $time = microtime(true) - $start; - - $data = HttpHelper::interpretResponse($response); - - return $this->formatter->formatHttpResult($response, $data, $connection, $time, $time, $statements); - } - - public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) - { - return TransactionHelper::retry(fn () => $this->beginTransaction(), $tsxHandler); - } - - public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) - { - return $this->writeTransaction($tsxHandler, $config); - } - - public function transaction(callable $tsxHandler, ?TransactionConfiguration $config = null) - { - if ($this->config->getAccessMode() === AccessMode::WRITE()) { - return $this->writeTransaction($tsxHandler, $config); - } - - return $this->readTransaction($tsxHandler, $config); - } - - /** - * @throws JsonException - */ - public function runStatement(Statement $statement, ?TransactionConfiguration $config = null) - { - return $this->runStatements([$statement], $config)->first(); - } - - /** - * @throws JsonException - */ - public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null) - { - return $this->runStatement(Statement::create($statement, $parameters), $config); - } - - /** - * @throws JsonException - */ - public function beginTransaction(?iterable $statements = null, ?TransactionConfiguration $config = null): UnmanagedTransactionInterface - { - $request = $this->requestFactory->resolve()->createRequest('POST', $this->uri->resolve()); - $connection = $this->pool->acquire($this->config); - /** @var HttpConnection */ - $connection = GeneratorHelper::getReturnFromGenerator($connection); - - $request = $this->formatter->decorateRequest($request, $connection); - $request->getBody()->write(HttpHelper::statementsToJson($connection, $this->formatter, $statements ?? [])); - $response = $connection->getImplementation()->sendRequest($request); - - $response = HttpHelper::interpretResponse($response); - if (isset($response->info) && $response->info instanceof stdClass) { - /** @var string */ - $url = $response->info->commit; - } else { - /** @var string */ - $url = $response->commit; - } - $path = str_replace('/commit', '', parse_url($url, PHP_URL_PATH)); - $uri = $request->getUri()->withPath($path); - $request = $request->withUri($uri); - - return $this->makeTransaction($connection, $request); - } - - /** - * @return HttpUnmanagedTransaction - */ - private function makeTransaction(HttpConnection $connection, RequestInterface $request): HttpUnmanagedTransaction - { - return new HttpUnmanagedTransaction( - $request, - $connection, - $this->streamFactory->resolve(), - $this->formatter - ); - } - - private function instantCommitRequest(RequestInterface $request): RequestInterface - { - $path = $request->getUri()->getPath().'/commit'; - $uri = $request->getUri()->withPath($path); - - return $request->withUri($uri); - } - - public function getLastBookmark(): Bookmark - { - return new Bookmark([]); - } -} diff --git a/src/Http/HttpUnmanagedTransaction.php b/src/Http/HttpUnmanagedTransaction.php deleted file mode 100644 index 7de93457..00000000 --- a/src/Http/HttpUnmanagedTransaction.php +++ /dev/null @@ -1,171 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use function array_intersect; -use function array_unique; - -use Laudis\Neo4j\Common\TransactionHelper; -use Laudis\Neo4j\Contracts\FormatterInterface; -use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; -use Laudis\Neo4j\Databags\Neo4jError; -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Exception\Neo4jException; -use Laudis\Neo4j\Types\CypherList; - -use function microtime; - -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamFactoryInterface; -use stdClass; - -/** - * @template T - * - * @implements UnmanagedTransactionInterface - */ -final class HttpUnmanagedTransaction implements UnmanagedTransactionInterface -{ - private bool $isCommitted = false; - - private bool $isRolledBack = false; - - /** - * @psalm-mutation-free - * - * @param FormatterInterface $formatter - */ - public function __construct( - /** @psalm-readonly */ - private readonly RequestInterface $request, - /** @psalm-readonly */ - private readonly HttpConnection $connection, - /** @psalm-readonly */ - private readonly StreamFactoryInterface $factory, - /** - * @psalm-readonly - */ - private readonly FormatterInterface $formatter - ) {} - - public function run(string $statement, iterable $parameters = []) - { - return $this->runStatement(new Statement($statement, $parameters)); - } - - public function runStatement(Statement $statement) - { - return $this->runStatements([$statement])->first(); - } - - public function runStatements(iterable $statements): CypherList - { - $request = $this->request->withMethod('POST'); - - $body = HttpHelper::statementsToJson($this->connection, $this->formatter, $statements); - - $request = $request->withBody($this->factory->createStream($body)); - $start = microtime(true); - $response = $this->connection->getImplementation()->sendRequest($request); - $total = microtime(true) - $start; - - $data = $this->handleResponse($response); - - return $this->formatter->formatHttpResult($response, $data, $this->connection, $total, $total, $statements); - } - - public function commit(iterable $statements = []): CypherList - { - $uri = $this->request->getUri(); - $request = $this->request->withUri($uri->withPath($uri->getPath().'/commit'))->withMethod('POST'); - - $content = HttpHelper::statementsToJson($this->connection, $this->formatter, $statements); - $request = $request->withBody($this->factory->createStream($content)); - - $start = microtime(true); - $response = $this->connection->getImplementation()->sendRequest($request); - $total = microtime(true) - $start; - - $data = $this->handleResponse($response); - - $this->isCommitted = true; - - return $this->formatter->formatHttpResult($response, $data, $this->connection, $total, $total, $statements); - } - - public function rollback(): void - { - $request = $this->request->withMethod('DELETE'); - $response = $this->connection->getImplementation()->sendRequest($request); - - $this->handleResponse($response); - - $this->isRolledBack = true; - } - - public function __destruct() - { - $this->connection->close(); - } - - public function isRolledBack(): bool - { - return $this->isRolledBack; - } - - public function isCommitted(): bool - { - return $this->isCommitted; - } - - public function isFinished(): bool - { - return $this->isRolledBack() || $this->isCommitted(); - } - - /** - * @throws Neo4jException - * - * @return never - */ - private function handleNeo4jException(Neo4jException $e): void - { - if (!$this->isFinished()) { - $classifications = array_map(static fn (Neo4jError $e) => $e->getClassification(), $e->getErrors()); - $classifications = array_unique($classifications); - - $intersection = array_intersect($classifications, TransactionHelper::ROLLBACK_CLASSIFICATIONS); - if ($intersection !== []) { - $this->isRolledBack = true; - } - } - - throw $e; - } - - /** - * @throws Neo4jException - */ - private function handleResponse(ResponseInterface $response): stdClass - { - try { - $data = HttpHelper::interpretResponse($response); - } catch (Neo4jException $e) { - $this->handleNeo4jException($e); - } - - return $data; - } -} diff --git a/src/Http/RequestFactory.php b/src/Http/RequestFactory.php deleted file mode 100644 index a8b04eb5..00000000 --- a/src/Http/RequestFactory.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Psr\Http\Message\RequestFactoryInterface; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; - -/** - * Request factory decorator to correctly configure a default Request. - */ -final class RequestFactory implements RequestFactoryInterface -{ - /** - * @psalm-mutation-free - */ - public function __construct( - /** @readonly */ - private readonly RequestFactoryInterface $requestFactory, - /** @readonly */ - private readonly AuthenticateInterface $authenticate, - /** @readonly */ - private readonly UriInterface $authUri, - /** @readonly */ - private readonly string $userAgent - ) {} - - public function createRequest(string $method, $uri): RequestInterface - { - $request = $this->requestFactory->createRequest($method, $uri); - $request = $this->authenticate->authenticateHttp($request, $this->authUri, $this->userAgent); - $uri = $request->getUri()->withUserInfo(''); - $port = $uri->getPort(); - if ($port === null) { - $port = $uri->getScheme() === 'https' ? 7473 : 7474; - $uri = $uri->withPort($port); - } - - return $request - ->withUri($uri) - ->withHeader('Accept', 'application/json;charset=UTF-8') - ->withHeader('Content-Type', 'application/json'); - } -} diff --git a/src/ParameterHelper.php b/src/ParameterHelper.php index d4667411..3d7c20fa 100644 --- a/src/ParameterHelper.php +++ b/src/ParameterHelper.php @@ -85,7 +85,7 @@ public static function asParameter( ): iterable|int|float|bool|string|stdClass|IStructure|null { return self::cypherMapToStdClass($value) ?? self::emptySequenceToArray($value) ?? - self::convertBoltConvertibles($value, $protocol) ?? + self::convertBoltConvertibles($value) ?? self::convertTemporalTypes($value, $protocol) ?? self::filledIterableToArray($value, $protocol) ?? self::stringAbleToString($value) ?? @@ -193,7 +193,9 @@ private static function iterableToArray(iterable $value, ConnectionProtocol $pro if (is_int($key) || is_string($key)) { $tbr[$key] = self::asParameter($val, $protocol); } else { - $msg = 'Iterable parameters must have an integer or string as key values, '.gettype($key).' received.'; + $msg = 'Iterable parameters must have an integer or string as key values, '.gettype( + $key + ).' received.'; throw new InvalidArgumentException($msg); } } @@ -201,9 +203,9 @@ private static function iterableToArray(iterable $value, ConnectionProtocol $pro return $tbr; } - private static function convertBoltConvertibles(mixed $value, ConnectionProtocol $protocol): ?IStructure + private static function convertBoltConvertibles(mixed $value): ?IStructure { - if ($protocol->isBolt() && $value instanceof BoltConvertibleInterface) { + if ($value instanceof BoltConvertibleInterface) { return $value->convertToBolt(); } @@ -212,31 +214,29 @@ private static function convertBoltConvertibles(mixed $value, ConnectionProtocol private static function convertTemporalTypes(mixed $value, ConnectionProtocol $protocol): ?IStructure { - if ($protocol->isBolt()) { - if ($value instanceof DateTimeInterface) { - if ($protocol->compare(ConnectionProtocol::BOLT_V44()) > 0) { - return new \Bolt\protocol\v5\structures\DateTimeZoneId( - $value->getTimestamp(), - ((int) $value->format('u')) * 1000, - $value->getTimezone()->getName() - ); - } - - return new DateTimeZoneId( + if ($value instanceof DateTimeInterface) { + if ($protocol->compare(ConnectionProtocol::BOLT_V44()) > 0) { + return new \Bolt\protocol\v5\structures\DateTimeZoneId( $value->getTimestamp(), ((int) $value->format('u')) * 1000, $value->getTimezone()->getName() ); } - if ($value instanceof DateInterval) { - return new Duration( - $value->y * 12 + $value->m, - $value->d, - $value->h * 60 * 60 * $value->i * 60 + $value->s * 60, - (int) ($value->f * 1000) - ); - } + return new DateTimeZoneId( + $value->getTimestamp(), + ((int) $value->format('u')) * 1000, + $value->getTimezone()->getName() + ); + } + + if ($value instanceof DateInterval) { + return new Duration( + $value->y * 12 + $value->m, + $value->d, + $value->h * 60 * 60 * $value->i * 60 + $value->s * 60, + (int) ($value->f * 1000) + ); } return null; diff --git a/tests/Integration/ClientIntegrationTest.php b/tests/Integration/ClientIntegrationTest.php index 1e1e072a..a02e8e87 100644 --- a/tests/Integration/ClientIntegrationTest.php +++ b/tests/Integration/ClientIntegrationTest.php @@ -276,7 +276,6 @@ public function testInvalidConnectionCheck(): void $client = ClientBuilder::create() ->withDriver('bolt', 'bolt://localboast') ->withDriver('neo4j', 'neo4j://localboast') - ->withDriver('http', 'http://localboast') ->build(); $exceptionThrownCount = 0; @@ -292,14 +291,8 @@ public function testInvalidConnectionCheck(): void self::assertInstanceOf(RuntimeException::class, $e); ++$exceptionThrownCount; } - try { - $client->verifyConnectivity('http'); - } catch (Exception $e) { - self::assertInstanceOf(RuntimeException::class, $e); - ++$exceptionThrownCount; - } - self::assertEquals(3, $exceptionThrownCount); + self::assertEquals(2, $exceptionThrownCount); } public function testValidConnectionCheck(): void