diff --git a/.github/workflows/integration-test-aura.yml b/.github/workflows/integration-test-aura.yml index 1d7da793..43dc9954 100644 --- a/.github/workflows/integration-test-aura.yml +++ b/.github/workflows/integration-test-aura.yml @@ -25,15 +25,15 @@ jobs: - uses: php-actions/composer@v6 with: progress: yes - php_version: 8.0 + php_version: 8.1 version: 2 - name: clean database run: CONNECTION=$CONNECTION php tests/clean-database.php - uses: php-actions/phpunit@v3 with: configuration: phpunit.xml.dist - php_version: 8.0 + php_version: 8.1 memory_limit: 1024M - version: 9 + version: 10 testsuite: Integration bootstrap: vendor/autoload.php diff --git a/.github/workflows/integration-test-cluster-neo4j-4.yml b/.github/workflows/integration-test-cluster-neo4j-4.yml index 2dbb58c4..0ad6fcb6 100644 --- a/.github/workflows/integration-test-cluster-neo4j-4.yml +++ b/.github/workflows/integration-test-cluster-neo4j-4.yml @@ -12,8 +12,8 @@ jobs: tests: runs-on: ubuntu-latest env: - CONNECTION: neo4j://neo4j:testtest@localhost:7688 - name: "Running on PHP 8.0 in a Neo4j 4.4 cluster" + CONNECTION: neo4j://neo4j:testtest@core1:7688 + name: "Running on PHP 8.1 in a Neo4j 4.4 cluster" steps: - uses: actions/checkout@v2 @@ -25,14 +25,14 @@ jobs: - uses: php-actions/composer@v6 with: progress: yes - php_version: 8.0 + php_version: 8.1 version: 2 - uses: php-actions/phpunit@v3 with: configuration: phpunit.xml.dist - php_version: 8.0 + php_version: 8.1 memory_limit: 1024M - version: 9 + version: 10 testsuite: Integration bootstrap: vendor/autoload.php diff --git a/.github/workflows/integration-test-cluster-neo4j-5.yml b/.github/workflows/integration-test-cluster-neo4j-5.yml index bab2e12a..8f97c44c 100644 --- a/.github/workflows/integration-test-cluster-neo4j-5.yml +++ b/.github/workflows/integration-test-cluster-neo4j-5.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest env: CONNECTION: neo4j://neo4j:testtest@localhost:7687 - name: "Running on PHP 8.0 with a Neo4j 5.10-enterprise cluster" + name: "Running on PHP 8.1 with a Neo4j 5.10-enterprise cluster" steps: - uses: actions/checkout@v2 @@ -25,14 +25,14 @@ jobs: - uses: php-actions/composer@v6 with: progress: yes - php_version: 8.0 + php_version: 8.1 version: 2 - uses: php-actions/phpunit@v3 with: configuration: phpunit.xml.dist - php_version: 8.0 + php_version: 8.1 memory_limit: 1024M - version: 9 + version: 10 testsuite: Integration bootstrap: vendor/autoload.php diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 530409d1..57edc2bc 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -23,13 +23,13 @@ jobs: - uses: php-actions/composer@v6 with: progress: yes - php_version: 8.2 + php_version: 8.1 version: 2 - uses: php-actions/phpunit@v3 with: configuration: phpunit.xml.dist - php_version: 8.2 + php_version: 8.1 memory_limit: 1024M - version: 9 + version: 10 testsuite: Unit bootstrap: vendor/autoload.php diff --git a/.gitignore b/.gitignore index 8e0fbc24..d81378b8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ composer.lock .env /docs/_build cachegrind.out.* +.phpunit.cache/ diff --git a/Dockerfile b/Dockerfile index 4637f93c..8f6d2797 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.0-cli +FROM php:8.1-cli RUN apt-get update \ && apt-get install -y \ libzip-dev \ diff --git a/composer.json b/composer.json index 4d0146ae..a08adc40 100644 --- a/composer.json +++ b/composer.json @@ -28,11 +28,12 @@ "psr/http-factory": "^1.0", "psr/http-client": "^1.0", "php-http/message": "^1.0", - "stefanak-michal/bolt": "^6.0", + "stefanak-michal/bolt": "^7.0.1", "symfony/polyfill-php80": "^1.2", "psr/simple-cache": ">=2.0", "ext-json": "*", - "ext-mbstring": "*" + "ext-mbstring": "*", + "psr/event-dispatcher": "^1.0" }, "provide": { "psr/simple-cache-implementation": "2.0|3.0" @@ -43,7 +44,7 @@ "composer-runtime-api": "Install composer 2 for auto detection of version in user agent" }, "require-dev": { - "phpunit/phpunit": "^9.0", + "phpunit/phpunit": "^10.0", "nyholm/psr7": "^1.3", "nyholm/psr7-server": "^1.0", "kriswallsmith/buzz": "^1.2", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f83458db..bd9af323 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,17 @@ - - - - ./tests/Integration - - - ./tests/Performance - - - ./tests/Unit - - - - - + + + + + ./tests/Integration + + + ./tests/Performance + + + ./tests/Unit + + + + + diff --git a/psalm-baseline.xml b/psalm-baseline.xml deleted file mode 100644 index 02b46e67..00000000 --- a/psalm-baseline.xml +++ /dev/null @@ -1,181 +0,0 @@ - - - - - $pass - - - - - enableSsl($uri->getHost(), $sslConfig, $config)]]]> - [$sslConfig, []] - - - - - - - - semaphore]]> - semaphore]]> - sem_get(hexdec($key), $max) - - - - - DatabaseInfo - - - - - Plan - - - - - ProfiledPlan - - - - - ResultSummary - - - - - ServerInfo - - - - - Statement - - - - - SummaryCounters - - - - - $meta - - - - - $coordinates - - - - - $value - $value - - - translateCypherList($value, $meta)]]> - [new CypherList($tbr), $meta] - - - array{0: OGMTypes, 1: HttpMetaInfo} - - - $milliseconds - $milliseconds - $secondsFraction - $time - $time - $timezone - $tzMinutes - - - - - $response - - - - - - - - array{x: float, y: float, z: float, srid: int, crs: Crs} - - - - - keyCache]]> - keyCache]]> - - - - - AbstractPoint - - - - - ]]> - - - - - ]]> - - - - - $connection - new Packer() - new Unpacker() - - - new V5(new Packer(), new Unpacker(), $connection, new ServerState()) - - - - - $item - - - ++$counter; - self::assertEquals(0, $counter); - - - - $counter - $key - - - - - IteratorAggregate - - - $item - - - ++$counter; - self::assertEquals(0, $counter); - 'x'][$key], $item);]]> - - - $counter - $key - - - - - resolver->getAddresses('8.8.8.8')]]> - resolver->getAddresses('bogus')]]> - resolver->getAddresses('test.ghlen.com')]]> - - - $records - - - - - Iterator - - - diff --git a/psalm.xml b/psalm.xml index 9368a22f..82f7d198 100755 --- a/psalm.xml +++ b/psalm.xml @@ -8,7 +8,8 @@ xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" hoistConstants="true" - errorBaseline="psalm-baseline.xml" + findUnusedBaselineEntry="true" + findUnusedCode="true" > diff --git a/src/Authentication/Authenticate.php b/src/Authentication/Authenticate.php index d0504cac..176bd28f 100644 --- a/src/Authentication/Authenticate.php +++ b/src/Authentication/Authenticate.php @@ -15,7 +15,6 @@ use function explode; -use Laudis\Neo4j\Contracts\AuthenticateInterface; use Psr\Http\Message\UriInterface; use function substr_count; @@ -72,7 +71,7 @@ public static function disabled(): NoAuth * * @pure */ - public static function fromUrl(UriInterface $uri): AuthenticateInterface + public static function fromUrl(UriInterface $uri): BasicAuth|NoAuth { /** * @psalm-suppress ImpureMethodCall Uri is a pure object: @@ -82,7 +81,9 @@ public static function fromUrl(UriInterface $uri): AuthenticateInterface $userInfo = $uri->getUserInfo(); if (substr_count($userInfo, ':') === 1) { - [$user, $pass] = explode(':', $userInfo); + /** @var array{0: string, 1: string} $explode */ + $explode = explode(':', $userInfo); + [$user, $pass] = $explode; return self::basic($user, $pass); } diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index 4456925b..d5dad27e 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -13,22 +13,23 @@ namespace Laudis\Neo4j\Authentication; -use function base64_encode; - -use Bolt\helpers\Auth; -use Bolt\protocol\Response; use Bolt\protocol\V4_4; use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; use Exception; +use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Exception\Neo4jException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; +use Stringable; /** * Authenticates connections using a basic username and password. + * + * @internal */ -final class BasicAuth implements AuthenticateInterface +final class BasicAuth implements AuthenticateInterface, Stringable { /** * @psalm-external-mutation-free @@ -39,37 +40,39 @@ public function __construct( ) {} /** - * @psalm-mutation-free + * @throws Exception + * + * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { - $combo = base64_encode($this->username.':'.$this->password); + if (method_exists($protocol, 'logon')) { + $protocol->hello(['user_agent' => $userAgent]); + $response = ResponseHelper::getResponse($protocol); + $protocol->logon([ + 'scheme' => 'basic', + 'principal' => $this->username, + 'credentials' => $this->password, + ]); + ResponseHelper::getResponse($protocol); - /** - * @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); - } + /** @var array{server: string, connection_id: string, hints: list} */ + return $response->content; + } else { + $protocol->hello([ + 'user_agent' => $userAgent, + 'scheme' => 'basic', + 'principal' => $this->username, + 'credentials' => $this->password, + ]); - /** - * @throws Exception - */ - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array - { - $response = $bolt->hello(Auth::basic($this->username, $this->password, $userAgent)); - if ($response->getSignature() === Response::SIGNATURE_FAILURE) { - throw Neo4jException::fromBoltResponse($response); + /** @var array{server: string, connection_id: string, hints: list} */ + return ResponseHelper::getResponse($protocol)->content; } - - /** @var array{server: string, connection_id: string, hints: list} */ - return $response->getContent(); } - public function toString(UriInterface $uri): string + public function __toString(): string { - return sprintf('Basic %s:%s@%s:%s', $this->username, '######', $uri->getHost(), $uri->getPort() ?? ''); + return sprintf('Basic %s:%s', $this->username, '######'); } } diff --git a/src/Authentication/KerberosAuth.php b/src/Authentication/KerberosAuth.php index e1b66f2e..152586de 100644 --- a/src/Authentication/KerberosAuth.php +++ b/src/Authentication/KerberosAuth.php @@ -13,21 +13,25 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\helpers\Auth; -use Bolt\protocol\Response; use Bolt\protocol\V4_4; use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Exception\Neo4jException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; use function sprintf; +use Stringable; + /** * Authenticates connections using a kerberos token. + * + * @internal */ -final class KerberosAuth implements AuthenticateInterface +final class KerberosAuth implements AuthenticateInterface, Stringable { /** * @psalm-external-mutation-free @@ -36,33 +40,32 @@ public function __construct( private readonly string $token ) {} - /** - * @psalm-mutation-free - */ - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { - /** - * @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', 'Kerberos '.$this->token) - ->withHeader('User-Agent', $userAgent); - } + if (method_exists($protocol, 'logon')) { + $protocol->logon([ + 'scheme' => 'kerberos', + 'principal' => '', + 'credentials' => $this->token, + ]); - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array - { - $response = $bolt->hello(Auth::kerberos($this->token, $userAgent)); - if ($response->getSignature() === Response::SIGNATURE_FAILURE) { - throw Neo4jException::fromBoltResponse($response); - } + /** @var array{server: string, connection_id: string, hints: list} */ + return ResponseHelper::getResponse($protocol)->content; + } else { + $protocol->hello([ + 'user_agent' => $userAgent, + 'scheme' => 'kerberos', + 'principal' => '', + 'credentials' => $this->token, + ]); - /** @var array{server: string, connection_id: string, hints: list} */ - return $response->getContent(); + /** @var array{server: string, connection_id: string, hints: list} */ + return ResponseHelper::getResponse($protocol)->content; + } } - public function toString(UriInterface $uri): string + public function __toString(): string { - return sprintf('Kerberos %s@%s:%s', $this->token, $uri->getHost(), $uri->getPort() ?? ''); + return sprintf('Kerberos %s', $this->token); } } diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php index e6391f69..983e5806 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -13,48 +13,45 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\helpers\Auth; -use Bolt\protocol\Response; use Bolt\protocol\V4_4; use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Exception\Neo4jException; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; - -use function sprintf; +use Stringable; /** * Doesn't authenticate connections. + * + * @internal */ -final class NoAuth implements AuthenticateInterface +final class NoAuth implements AuthenticateInterface, Stringable { - /** - * @psalm-mutation-free - */ - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface - { - /** - * @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('User-Agent', $userAgent); - } - - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { - $response = $bolt->hello(Auth::none($userAgent)); - if ($response->getSignature() === Response::SIGNATURE_FAILURE) { - throw Neo4jException::fromBoltResponse($response); + if (method_exists($protocol, 'logon')) { + $protocol->logon([ + 'scheme' => 'none', + ]); + + /** @var array{server: string, connection_id: string, hints: list} */ + return ResponseHelper::getResponse($protocol)->content; + } else { + $protocol->hello([ + 'user_agent' => $userAgent, + 'scheme' => 'none', + ]); + + /** @var array{server: string, connection_id: string, hints: list} */ + return ResponseHelper::getResponse($protocol)->content; } - - /** @var array{server: string, connection_id: string, hints: list} */ - return $response->getContent(); } - public function toString(UriInterface $uri): string + public function __toString(): string { - return sprintf('No Auth %s:%s', $uri->getHost(), $uri->getPort() ?? ''); + return 'No Auth'; } } diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php index a28718a7..ac5b1374 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -13,18 +13,24 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\helpers\Auth; -use Bolt\protocol\Response; use Bolt\protocol\V4_4; use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Exception\Neo4jException; use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; use function sprintf; -final class OpenIDConnectAuth implements AuthenticateInterface +use Stringable; + +/** + * @internal + */ +final class OpenIDConnectAuth implements AuthenticateInterface, Stringable { /** * @psalm-external-mutation-free @@ -34,32 +40,31 @@ public function __construct( ) {} /** - * @psalm-mutation-free + * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { - /** - * @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', 'Bearer '.$this->token) - ->withHeader('User-Agent', $userAgent); - } + if (method_exists($protocol, 'logon')) { + $protocol->logon([ + 'scheme' => 'bearer', + 'credentials' => $this->token, + ]); + /** @var array{server: string, connection_id: string, hints: list} */ + return ResponseHelper::getResponse($protocol)->content; + } else { + $protocol->hello([ + 'user_agent' => $userAgent, + 'scheme' => 'bearer', + 'credentials' => $this->token, + ]); - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array - { - $response = $bolt->hello(Auth::bearer($this->token, $userAgent)); - if ($response->getSignature() === Response::SIGNATURE_FAILURE) { - throw Neo4jException::fromBoltResponse($response); + /** @var array{server: string, connection_id: string, hints: list} */ + return ResponseHelper::getResponse($protocol)->content; } - - /** @var array{server: string, connection_id: string, hints: list} */ - return $response->getContent(); } - public function toString(UriInterface $uri): string + public function __toString(): string { - return sprintf('OpenId %s@%s:%s', $this->token, $uri->getHost(), $uri->getPort() ?? ''); + return sprintf('OpenId %s', $this->token); } } diff --git a/src/Basic/Client.php b/src/Basic/Client.php deleted file mode 100644 index afb6db66..00000000 --- a/src/Basic/Client.php +++ /dev/null @@ -1,104 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Basic; - -use Laudis\Neo4j\Contracts\ClientInterface; -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Databags\SummarizedResult; -use Laudis\Neo4j\Databags\TransactionConfiguration; -use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; - -/** - * @implements ClientInterface> - */ -final class Client implements ClientInterface -{ - /** - * @param ClientInterface> $client - */ - public function __construct( - private readonly ClientInterface $client - ) {} - - public function run(string $statement, iterable $parameters = [], ?string $alias = null): SummarizedResult - { - return $this->client->run($statement, $parameters, $alias); - } - - public function runStatement(Statement $statement, ?string $alias = null): SummarizedResult - { - return $this->client->runStatement($statement, $alias); - } - - public function runStatements(iterable $statements, ?string $alias = null): CypherList - { - return $this->client->runStatements($statements, $alias); - } - - public function beginTransaction(?iterable $statements = null, ?string $alias = null, ?TransactionConfiguration $config = null): UnmanagedTransaction - { - return new UnmanagedTransaction($this->client->beginTransaction($statements, $alias, $config)); - } - - public function getDriver(?string $alias): Driver - { - $driver = $this->client->getDriver($alias); - if ($driver instanceof Driver) { - return $driver; - } - - return new Driver($driver); - } - - public function hasDriver(string $alias): bool - { - return $this->client->hasDriver($alias); - } - - public function writeTransaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null) - { - return $this->client->writeTransaction($tsxHandler, $alias, $config); - } - - public function readTransaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null) - { - return $this->client->readTransaction($tsxHandler, $alias, $config); - } - - public function transaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null) - { - return $this->client->transaction($tsxHandler, $alias, $config); - } - - public function verifyConnectivity(?string $driver = null): bool - { - return $this->client->verifyConnectivity($driver); - } - - public function bindTransaction(?string $alias = null, ?TransactionConfiguration $config = null): void - { - $this->client->bindTransaction($alias, $config); - } - - public function commitBoundTransaction(?string $alias = null, int $depth = 1): void - { - $this->client->commitBoundTransaction($alias, $depth); - } - - public function rollbackBoundTransaction(?string $alias = null, int $depth = 1): void - { - $this->client->rollbackBoundTransaction($alias, $depth); - } -} diff --git a/src/Basic/Driver.php b/src/Basic/Driver.php deleted file mode 100644 index ca76954d..00000000 --- a/src/Basic/Driver.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Basic; - -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Contracts\DriverInterface; -use Laudis\Neo4j\Databags\DriverConfiguration; -use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Databags\SummarizedResult; -use Laudis\Neo4j\DriverFactory; -use Laudis\Neo4j\Formatter\SummarizedResultFormatter; -use Laudis\Neo4j\Types\CypherMap; -use Psr\Http\Message\UriInterface; - -/** - * @implements DriverInterface> - */ -final class Driver implements DriverInterface -{ - /** - * @param DriverInterface> $driver - * - * @psalm-external-mutation-free - */ - public function __construct( - private readonly DriverInterface $driver - ) {} - - /** - * @psalm-mutation-free - */ - public function createSession(?SessionConfiguration $config = null): Session - { - return new Session($this->driver->createSession($config)); - } - - public function verifyConnectivity(?SessionConfiguration $config = null): bool - { - return $this->driver->verifyConnectivity($config); - } - - public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null): self - { - /** @var DriverInterface> */ - $driver = DriverFactory::create($uri, $configuration, $authenticate, SummarizedResultFormatter::create()); - - return new self($driver); - } -} diff --git a/src/Basic/Session.php b/src/Basic/Session.php deleted file mode 100644 index cb31416f..00000000 --- a/src/Basic/Session.php +++ /dev/null @@ -1,88 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Basic; - -use Laudis\Neo4j\Contracts\SessionInterface; -use Laudis\Neo4j\Databags\Bookmark; -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Databags\SummarizedResult; -use Laudis\Neo4j\Databags\TransactionConfiguration; -use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; - -/** - * @implements SessionInterface> - */ -final class Session implements SessionInterface -{ - /** - * @param SessionInterface> $session - */ - public function __construct( - private readonly SessionInterface $session - ) {} - - /** - * @param iterable $statements - * - * @return CypherList> - */ - public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList - { - return $this->session->runStatements($statements, $config); - } - - /** - * @return SummarizedResult - */ - public function runStatement(Statement $statement, ?TransactionConfiguration $config = null): SummarizedResult - { - return $this->session->runStatement($statement, $config); - } - - /** - * @param iterable $parameters - * - * @return SummarizedResult - */ - public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null): SummarizedResult - { - return $this->session->run($statement, $parameters, $config); - } - - public function beginTransaction(?iterable $statements = null, ?TransactionConfiguration $config = null): UnmanagedTransaction - { - return new UnmanagedTransaction($this->session->beginTransaction($statements, $config)); - } - - public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) - { - return $this->session->writeTransaction($tsxHandler, $config); - } - - public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) - { - return $this->session->readTransaction($tsxHandler, $config); - } - - public function transaction(callable $tsxHandler, ?TransactionConfiguration $config = null) - { - return $this->session->writeTransaction($tsxHandler, $config); - } - - public function getLastBookmark(): Bookmark - { - return $this->session->getLastBookmark(); - } -} diff --git a/src/Basic/UnmanagedTransaction.php b/src/Basic/UnmanagedTransaction.php deleted file mode 100644 index 233e582d..00000000 --- a/src/Basic/UnmanagedTransaction.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Basic; - -use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Databags\SummarizedResult; -use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; - -/** - * @implements UnmanagedTransactionInterface> - */ -final class UnmanagedTransaction implements UnmanagedTransactionInterface -{ - /** - * @param UnmanagedTransactionInterface> $tsx - */ - public function __construct( - private readonly UnmanagedTransactionInterface $tsx - ) {} - - /** - * @param iterable $parameters - * - * @return SummarizedResult - */ - public function run(string $statement, iterable $parameters = []): SummarizedResult - { - return $this->tsx->run($statement, $parameters); - } - - /** - * @return SummarizedResult - */ - public function runStatement(Statement $statement): SummarizedResult - { - return $this->tsx->runStatement($statement); - } - - /** - * @param iterable $statements - * - * @return CypherList> - */ - public function runStatements(iterable $statements): CypherList - { - return $this->tsx->runStatements($statements); - } - - /** - * @param iterable $statements - * - * @return CypherList> - */ - public function commit(iterable $statements = []): CypherList - { - return $this->tsx->commit($statements); - } - - public function rollback(): void - { - $this->tsx->rollback(); - } - - public function isCommitted(): bool - { - return $this->tsx->isCommitted(); - } - - public function isRolledBack(): bool - { - return $this->tsx->isRolledBack(); - } - - public function isFinished(): bool - { - return $this->tsx->isFinished(); - } -} diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 66b45f8d..3ad196f0 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -13,29 +13,48 @@ namespace Laudis\Neo4j\Bolt; +use Bolt\enum\ServerState; +use Bolt\enum\Signature; use Bolt\protocol\Response; -use Bolt\protocol\ServerState; use Bolt\protocol\V4_4; use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Bolt\Messages\Begin; +use Laudis\Neo4j\Bolt\Messages\Commit; +use Laudis\Neo4j\Bolt\Messages\Discard; +use Laudis\Neo4j\Bolt\Messages\Reset; +use Laudis\Neo4j\Bolt\Messages\Rollback; +use Laudis\Neo4j\Bolt\Messages\Route; +use Laudis\Neo4j\Bolt\Messages\Run; +use Laudis\Neo4j\Bolt\Responses\CommitResponse; +use Laudis\Neo4j\Bolt\Responses\ResultSuccessResponse; +use Laudis\Neo4j\Bolt\Responses\RouteResponse; +use Laudis\Neo4j\Bolt\Responses\RunResponse; use Laudis\Neo4j\Common\ConnectionConfiguration; -use Laudis\Neo4j\Contracts\AuthenticateInterface; +use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; +use Laudis\Neo4j\Contracts\MessageInterface; +use Laudis\Neo4j\Databags\Bookmark; use Laudis\Neo4j\Databags\BookmarkHolder; -use Laudis\Neo4j\Databags\DatabaseInfo; use Laudis\Neo4j\Enum\AccessMode; -use Laudis\Neo4j\Enum\ConnectionProtocol; +use Laudis\Neo4j\Enum\QueryTypeEnum; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Results\Result; +use Laudis\Neo4j\Results\ResultCursor; use Laudis\Neo4j\Types\CypherList; -use Psr\Http\Message\UriInterface; +use Throwable; use WeakReference; /** - * @implements ConnectionInterface + * @internal * - * @psalm-import-type BoltMeta from FormatterInterface + * @psalm-suppress PossiblyUndefinedStringArrayOffset We temporarily suppress these warnings as we are translating the weakly typed bolt library to the driver. + * @psalm-suppress MixedArgument */ -class BoltConnection implements ConnectionInterface +final class BoltConnection { /** * @note We are using references to "subscribed results" to maintain backwards compatibility and try and strike @@ -45,89 +64,27 @@ class BoltConnection implements ConnectionInterface * edge cases where the result set will be pulled or discarded when it is not strictly necessary, and we * should introduce a "manual" mode later down the road to allow the end users to optimise the result * consumption themselves. - * A great moment to do this would be when neo4j 5 is released as it will presumably allow us to do more - * stuff with PULL and DISCARD messages. * - * @var list> + * @var list> */ private array $subscribedResults = []; - /** - * @return array{0: V4_4|V5, 1: Connection} - */ - public function getImplementation(): array - { - return [$this->boltProtocol, $this->connection]; - } - /** * @psalm-mutation-free */ public function __construct( - private V4_4|V5 $boltProtocol, - private readonly Connection $connection, - private readonly AuthenticateInterface $auth, - private readonly string $userAgent, - /** @psalm-readonly */ + private readonly V4_4|V5|V5_1|V5_2|V5_3 $boltProtocol, private readonly ConnectionConfiguration $config - ) {} - - public function getEncryptionLevel(): string - { - return $this->connection->getEncryptionLevel(); - } - - /** - * @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 + * @return ConnectionConfiguration */ - public function getProtocol(): ConnectionProtocol + public function getConfig(): ConnectionConfiguration { - 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(); - } - - public function getAuthentication(): AuthenticateInterface - { - return $this->auth; + return $this->config; } /** @@ -135,20 +92,15 @@ public function getAuthentication(): AuthenticateInterface */ public function isOpen(): bool { - return !in_array($this->protocol()->serverState->get(), ['DISCONNECTED', 'DEFUNCT'], true); - } - - public function setTimeout(float $timeout): void - { - $this->connection->setTimeout($timeout); + return !in_array($this->protocol()->serverState, [ServerState::DISCONNECTED, ServerState::DEFUNCT], true); } public function consumeResults(): void { foreach ($this->subscribedResults as $result) { $result = $result->get(); - if ($result) { - $result->preload(); + if ($result !== null) { + $result->consume(); } } @@ -163,29 +115,54 @@ public function consumeResults(): void */ public function reset(): void { - $response = $this->protocol()->reset() - ->getResponse(); + $this->sendMessage(new Reset()); + $this->subscribedResults = []; + } + + public function sendMessage(MessageInterface $message): Response + { + $message->send($this->protocol()); + + $response = $this->getResponse(); $this->assertNoFailure($response); - $this->subscribedResults = []; + return $response; + } + + public function getResponse(): Response + { + $response = $this->protocol()->getResponse(); + if ($response->signature === Signature::FAILURE) { + throw Neo4jException::fromBoltResponse($response); + } + + return $response; } /** * Begins a transaction. * + * + * @param array $txMetadata + * @param list $notificationsDisabledCategories + * * Any of the preconditioned states are: 'READY', 'INTERRUPTED'. */ - public function begin(?string $database, ?float $timeout, BookmarkHolder $holder): void + public function begin( + BookmarkHolder $bookmarks, + int|null $txTimeout, + array $txMetadata, + AccessMode|null $mode, + string|null $database, + string|null $impersonatedUser, + string|null $notificationsMinimumSeverity, + array $notificationsDisabledCategories + ): void { $this->consumeResults(); - $extra = $this->buildRunExtra($database, $timeout, $holder, AccessMode::WRITE()); - $response = $this->protocol() - ->begin($extra) - ->getResponse(); - - $this->assertNoFailure($response); + $this->sendMessage(new Begin($bookmarks, $txTimeout, $txMetadata, $mode, $database, $impersonatedUser, $notificationsMinimumSeverity, $notificationsDisabledCategories)); } /** @@ -193,34 +170,41 @@ public function begin(?string $database, ?float $timeout, BookmarkHolder $holder * * Any of the preconditioned states are: 'STREAMING', 'TX_STREAMING', 'FAILED', 'INTERRUPTED'. */ - public function discard(?int $qid): void + public function discard(int $n, ?int $qid): ResultSuccessResponse { - $extra = $this->buildResultExtra(null, $qid); - $bolt = $this->protocol(); + $result = $this->sendMessage(new Discard($n, $qid)); - $response = $bolt->discard($extra) - ->getResponse(); - - $this->assertNoFailure($response); + return $this->createResultSuccessResponse($result); } /** * Runs a query/statement. * - * Any of the preconditioned states are: 'STREAMING', 'TX_STREAMING', 'FAILED', 'INTERRUPTED'. + * @param array $parameters + * @param array $txMetadata + * @param list $notificationsDisabledCategories * - * @return BoltMeta + * Any of the preconditioned states are: 'STREAMING', 'TX_STREAMING', 'FAILED', 'INTERRUPTED'. */ - public function run(string $text, array $parameters, ?string $database, ?float $timeout, BookmarkHolder $holder, ?AccessMode $mode): array - { - $extra = $this->buildRunExtra($database, $timeout, $holder, $mode); - $response = $this->protocol()->run($text, $parameters, $extra) - ->getResponse(); - - $this->assertNoFailure($response); - - /** @var BoltMeta */ - return $response->getContent(); + public function run( + string $text, + array $parameters, + BookmarkHolder $bookmarks, + int|null $txTimeout, + array|null $txMetadata, + AccessMode|null $mode, + string|null $database, + string|null $impersonatedUser, + string|null $notificationsMinimumSeverity, + array|null $notificationsDisabledCategories + ): RunResponse { + $response = $this->sendMessage(new Run($text, $parameters, $bookmarks, $txTimeout, $txMetadata, $mode, $database, $impersonatedUser, $notificationsMinimumSeverity, $notificationsDisabledCategories)); + + return new RunResponse( + $response->content['fields'], + $response->content['t_first'], + $response->content['qid'], + ); } /** @@ -228,14 +212,13 @@ public function run(string $text, array $parameters, ?string $database, ?float $ * * Any of the preconditioned states are: 'TX_READY', 'INTERRUPTED'. */ - public function commit(): void + public function commit(): CommitResponse { $this->consumeResults(); - $response = $this->protocol() - ->commit() - ->getResponse(); - $this->assertNoFailure($response); + $response = $this->sendMessage(new Commit()); + + return new CommitResponse(array_key_exists('bookmark', $response->content) ? new Bookmark($response->content['bookmark']) : null); } /** @@ -246,107 +229,72 @@ public function commit(): void public function rollback(): void { $this->consumeResults(); - $response = $this->protocol() - ->rollback() - ->getResponse(); - $this->assertNoFailure($response); + $this->sendMessage(new Rollback()); } - public function protocol(): V4_4|V5 + public function protocol(): V4_4|V5|V5_1|V5_2|V5_3|V5_4 { return $this->boltProtocol; } - /** - * Pulls a result set. - * - * Any of the preconditioned states are: 'TX_READY', 'INTERRUPTED'. - * - * @return non-empty-list - */ - public function pull(?int $qid, ?int $fetchSize): array - { - $extra = $this->buildResultExtra($fetchSize, $qid); - - $tbr = []; - foreach ($this->protocol()->pull($extra)->getResponses() as $response) { - $this->assertNoFailure($response); - - $tbr[] = $response->getContent(); - } - - /** @var non-empty-list */ - return $tbr; - } - public function __destruct() { - if (!$this->protocol()->serverState->is(ServerState::FAILED) && $this->isOpen()) { - if ($this->protocol()->serverState->is(ServerState::STREAMING, ServerState::TX_STREAMING)) { - $this->consumeResults(); - } + try { + if ($this->boltProtocol->serverState === ServerState::FAILED && $this->isOpen()) { + if ($this->protocol()->serverState === ServerState::STREAMING || $this->protocol()->serverState === ServerState::TX_STREAMING) { + $this->consumeResults(); + } - $this->protocol()->goodbye(); + $this->protocol()->goodbye(); - unset($this->boltProtocol); // has to be set to null as the sockets don't recover nicely contrary to what the underlying code might lead you to believe; + unset($this->boltProtocol); // has to be set to null as the sockets don't recover nicely contrary to what the underlying code might lead you to believe; + } + } catch (Throwable) { } } - private function buildRunExtra(?string $database, ?float $timeout, BookmarkHolder $holder, ?AccessMode $mode): array + public function getServerState(): ServerState { - $extra = []; - if ($database) { - $extra['db'] = $database; - } - if ($timeout) { - $extra['tx_timeout'] = (int) ($timeout * 1000); - } - - if (!$holder->getBookmark()->isEmpty()) { - $extra['bookmarks'] = $holder->getBookmark()->values(); - } - - if ($mode) { - $extra['mode'] = AccessMode::WRITE() === $mode ? 'w' : 'r'; - } - - return $extra; + return $this->protocol()->serverState; } - private function buildResultExtra(?int $fetchSize, ?int $qid): array - { - $extra = []; - if ($fetchSize !== null) { - $extra['n'] = $fetchSize; - } - - if ($qid !== null) { - $extra['qid'] = $qid; - } - - return $extra; - } - - public function getServerState(): string + public function subscribeResult(CypherList $result): void { - return $this->protocol()->serverState->get(); + $this->subscribedResults[] = WeakReference::create($result); } - public function subscribeResult(CypherList $result): void + private function assertNoFailure(Response $response): void { - $this->subscribedResults[] = WeakReference::create($result); + if ($response->signature === Signature::FAILURE) { + $this->protocol()->reset()->getResponse(); // what if the reset fails? what should be expected behaviour? + throw Neo4jException::fromBoltResponse($response); + } } - public function getUserAgent(): string + /** + * @param Response $response + * @return ResultSuccessResponse + */ + private function createResultSuccessResponse(Response $response): ResultSuccessResponse { - return $this->userAgent; + return new ResultSuccessResponse( + has_more: $response->content['has_more'], + bookmark: array_key_exists('bookmark', $response->content) ? new Bookmark($response->content['bookmark']) : null, + db: $response->content['db'] ?? null, + notification: $response->content['notification'] ?? null, + plan: $response->content['plan'] ?? null, + profile: $response->content['profile'] ?? null, + stats: $response->content['stats'] ?? null, + t_last: $response->content['t_last'] ?? null, + t_first: $response->content['t_first'] ?? null, + type: array_key_exists('type', $response->content) ? QueryTypeEnum::from($response->content['type']) : null, + ); } - private function assertNoFailure(Response $response): void + public function route(): RouteResponse { - if ($response->getSignature() === Response::SIGNATURE_FAILURE) { - throw Neo4jException::fromBoltResponse($response); - } + $response = $this->sendMessage(new Route()); + return new RouteResponse(); } } diff --git a/src/Bolt/BoltDriver.php b/src/Bolt/BoltDriver.php index 23bb1bad..e6ca3902 100644 --- a/src/Bolt/BoltDriver.php +++ b/src/Bolt/BoltDriver.php @@ -22,50 +22,29 @@ 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 Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Psr\Http\Message\UriInterface; use Throwable; /** * Drives a singular bolt connections. - * - * @template T - * - * @implements DriverInterface - * - * @psalm-import-type OGMResults from OGMFormatter */ final class BoltDriver implements DriverInterface { /** - * @param FormatterInterface $formatter - * * @psalm-mutation-free */ public function __construct( - private readonly UriInterface $parsedUrl, - private readonly ConnectionPool $pool, - private readonly FormatterInterface $formatter + private readonly UriInterface $parsedUrl, + private readonly ConnectionPool $pool, + private readonly SummarizedResultFormatter $formatter ) {} - /** - * @template U - * - * @param FormatterInterface $formatter - * - * @return ( - * func_num_args() is 5 - * ? self - * : self - * ) - * - * @psalm-suppress MixedReturnTypeCoercion - */ - public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null, FormatterInterface $formatter = null): self + public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null): self { if (is_string($uri)) { $uri = Uri::create($uri); @@ -75,11 +54,10 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co $authenticate ??= Authenticate::fromUrl($uri); $semaphore = $configuration->getSemaphoreFactory()->create($uri, $configuration); - /** @psalm-suppress InvalidArgument */ return new self( $uri, ConnectionPool::create($uri, $authenticate, $configuration, $semaphore), - $formatter ?? OGMFormatter::create(), + SummarizedResultFormatter::create(), ); } diff --git a/src/Bolt/BoltResult.php b/src/Bolt/BoltResult.php deleted file mode 100644 index dd243b28..00000000 --- a/src/Bolt/BoltResult.php +++ /dev/null @@ -1,153 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Bolt; - -use function array_splice; -use function count; - -use Generator; - -use function in_array; - -use Iterator; -use Laudis\Neo4j\Contracts\FormatterInterface; - -/** - * @psalm-import-type BoltCypherStats from FormatterInterface - * - * @implements Iterator - */ -final class BoltResult implements Iterator -{ - /** @var list */ - private array $rows = []; - private ?array $meta = null; - /** @var list<(callable(array):void)> */ - private array $finishedCallbacks = []; - - public function __construct( - private readonly BoltConnection $connection, - private readonly int $fetchSize, - private readonly int $qid - ) {} - - public function getFetchSize(): int - { - return $this->fetchSize; - } - - private ?Generator $it = null; - - /** - * @param callable(array):void $finishedCallback - */ - public function addFinishedCallback(callable $finishedCallback): void - { - $this->finishedCallbacks[] = $finishedCallback; - } - - /** - * @return Generator - */ - public function getIt(): Generator - { - if ($this->it === null) { - $this->it = $this->iterator(); - } - - return $this->it; - } - - /** - * @return Generator - */ - public function iterator(): Generator - { - $i = 0; - while ($this->meta === null) { - $this->fetchResults(); - foreach ($this->rows as $row) { - yield $i => $row; - ++$i; - } - } - - foreach ($this->finishedCallbacks as $finishedCallback) { - $finishedCallback($this->meta); - } - } - - public function consume(): array - { - while ($this->valid()) { - $this->next(); - } - - return $this->meta ?? []; - } - - private function fetchResults(): void - { - $meta = $this->connection->pull($this->qid, $this->fetchSize); - - /** @var list $rows */ - $rows = array_splice($meta, 0, count($meta) - 1); - $this->rows = $rows; - - /** @var array{0: array} $meta */ - if (!array_key_exists('has_more', $meta[0]) || $meta[0]['has_more'] === false) { - $this->meta = $meta[0]; - } - } - - /** - * @return list - */ - public function current(): array - { - return $this->getIt()->current(); - } - - public function next(): void - { - $this->getIt()->next(); - } - - public function key(): int - { - return $this->getIt()->key(); - } - - public function valid(): bool - { - return $this->getIt()->valid(); - } - - public function rewind(): void - { - // Rewind is impossible - } - - public function __destruct() - { - if ($this->meta === null && in_array($this->connection->getServerState(), ['STREAMING', 'TX_STREAMING'], true)) { - $this->discard(); - } - } - - public function discard(): void - { - $this->connection->discard($this->qid === -1 ? null : $this->qid); - } -} diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index cf649e68..5bf6618b 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -19,10 +19,11 @@ use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\SessionConfiguration; use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\ParameterHelper; -use Laudis\Neo4j\Types\AbstractCypherSequence; use Laudis\Neo4j\Types\CypherList; use function microtime; @@ -31,12 +32,6 @@ /** * Manages a transaction over the bolt protocol. - * - * @template T - * - * @implements UnmanagedTransactionInterface - * - * @psalm-import-type BoltMeta from FormatterInterface */ final class BoltUnmanagedTransaction implements UnmanagedTransactionInterface { @@ -44,37 +39,19 @@ final class BoltUnmanagedTransaction implements UnmanagedTransactionInterface private bool $isCommitted = false; - /** - * @param FormatterInterface $formatter - */ public function __construct( - /** @psalm-readonly */ private readonly ?string $database, - /** - * @psalm-readonly - */ - private readonly FormatterInterface $formatter, - /** @psalm-readonly */ + private readonly SummarizedResultFormatter $formatter, private readonly BoltConnection $connection, private readonly SessionConfiguration $config, private readonly TransactionConfiguration $tsxConfig, private readonly BookmarkHolder $bookmarkHolder ) {} - public function commit(iterable $statements = []): CypherList + public function commit(): void { - // Force the results to pull all the results. - // After a commit, the connection will be in the ready state, making it impossible to use PULL - $tbr = $this->runStatements($statements)->each(static function ($list) { - if ($list instanceof AbstractCypherSequence) { - $list->preload(); - } - }); - $this->connection->commit(); $this->isCommitted = true; - - return $tbr; } public function rollback(): void @@ -86,7 +63,7 @@ public function rollback(): void /** * @throws Throwable */ - public function run(string $statement, iterable $parameters = []) + public function run(string $statement, iterable $parameters = []): SummarizedResult { return $this->runStatement(new Statement($statement, $parameters)); } @@ -94,19 +71,23 @@ public function run(string $statement, iterable $parameters = []) /** * @throws Throwable */ - public function runStatement(Statement $statement) + public function runStatement(Statement $statement): SummarizedResult { - $parameters = ParameterHelper::formatParameters($statement->getParameters(), $this->connection->getProtocol()); + $parameters = ParameterHelper::formatParameters($statement->getParameters(), $this->connection->getConfig()->getProtocol()); $start = microtime(true); try { $meta = $this->connection->run( $statement->getText(), $parameters->toArray(), - $this->database, - $this->tsxConfig->getTimeout(), $this->bookmarkHolder, - $this->config->getAccessMode() + $this->tsxConfig->getTimeout(), + $this->tsxConfig->getMetaData(), + $this->config->getAccessMode(), + $this->database, + null, + null, + null, ); } catch (Throwable $e) { $this->isRolledBack = true; diff --git a/src/Bolt/Connection.php b/src/Bolt/Connection.php index 196fb1a1..c7c0ede9 100644 --- a/src/Bolt/Connection.php +++ b/src/Bolt/Connection.php @@ -14,12 +14,9 @@ namespace Laudis\Neo4j\Bolt; use Bolt\connection\IConnection; -use Bolt\protocol\AProtocol; -class Connection +final class Connection { - private ?AProtocol $protocol = null; - /** * @param ''|'s'|'ssc' $ssl */ diff --git a/src/Bolt/ConnectionPool.php b/src/Bolt/ConnectionPool.php index 82aadb4e..a0cfaff5 100644 --- a/src/Bolt/ConnectionPool.php +++ b/src/Bolt/ConnectionPool.php @@ -13,6 +13,7 @@ namespace Laudis\Neo4j\Bolt; +use Bolt\enum\ServerState; use Generator; use Laudis\Neo4j\BoltFactory; use Laudis\Neo4j\Contracts\AuthenticateInterface; @@ -30,9 +31,6 @@ use function shuffle; -/** - * @implements ConnectionPoolInterface - */ final class ConnectionPool implements ConnectionPoolInterface { /** @var list */ @@ -92,7 +90,7 @@ public function acquire(SessionConfiguration $config): Generator })(); } - public function release(ConnectionInterface $connection): void + public function release(BoltConnection $connection): void { $this->semaphore->post(); @@ -108,7 +106,7 @@ public function release(ConnectionInterface $connection): void /** * @return BoltConnection|null */ - private function returnAnyAvailableConnection(SessionConfiguration $config): ?ConnectionInterface + private function returnAnyAvailableConnection(SessionConfiguration $config): ?BoltConnection { $streamingConnection = null; $requiresReconnectConnection = null; @@ -132,7 +130,7 @@ private function returnAnyAvailableConnection(SessionConfiguration $config): ?Co // results open that aren't consumed yet. // https://github.com/neo4j-php/neo4j-php-client/issues/146 // NOTE: we cannot work with TX_STREAMING as we cannot force the transaction to implicitly close. - if ($streamingConnection === null && $activeConnection->getServerState() === 'STREAMING') { + if ($streamingConnection === null && $activeConnection->getServerState() === ServerState::STREAMING) { if ($this->factory->canReuseConnection($activeConnection, $this->data, $config)) { $streamingConnection = $activeConnection; if (method_exists($streamingConnection, 'consumeResults')) { diff --git a/src/Bolt/Messages/AbstractMessage.php b/src/Bolt/Messages/AbstractMessage.php new file mode 100644 index 00000000..c96c08bd --- /dev/null +++ b/src/Bolt/Messages/AbstractMessage.php @@ -0,0 +1,41 @@ +toArray()); + } + + public function __toString() + { + return strtoupper(__CLASS__) . ' => ' . $this->jsonSerialize(); + } + + public function toArray(): array + { + return [ + 'message' => strtoupper(__CLASS__), + 'extra' => $this->getProperties(), + ]; + } + + private function getProperties(): array + { + $reflection = new \ReflectionClass($this); + $properties = $reflection->getProperties(); + $tbr = []; + foreach ($properties as $property) { + /** @var string|int|float|list|null */ + $value = $property->getValue($this); + if ($value !== null) { + $tbr[$property->getName()] = $value; + } + } + + return $tbr; + } +} diff --git a/src/Bolt/Messages/Begin.php b/src/Bolt/Messages/Begin.php new file mode 100644 index 00000000..265c7933 --- /dev/null +++ b/src/Bolt/Messages/Begin.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; + +class Begin extends TransactionMessage +{ + protected function sendWithPreDecoratedExtraData(array $extra, V4_4|V5|V5_1|V5_2|V5_3|V5_4 $bolt): void + { + $bolt->begin($extra); + } +} diff --git a/src/Bolt/Messages/Commit.php b/src/Bolt/Messages/Commit.php new file mode 100644 index 00000000..04d78893 --- /dev/null +++ b/src/Bolt/Messages/Commit.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Contracts\MessageInterface; + +class Commit extends AbstractMessage implements MessageInterface +{ + public function send(V4_4|V5|V5_2|V5_1|V5_3|V5_4 $bolt): void + { + $bolt->commit(); + } +} diff --git a/src/Bolt/Messages/Discard.php b/src/Bolt/Messages/Discard.php new file mode 100644 index 00000000..522b4212 --- /dev/null +++ b/src/Bolt/Messages/Discard.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Contracts\MessageInterface; + +/** + * @internal + * + * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-discard + */ +class Discard extends AbstractMessage implements MessageInterface +{ + public function __construct( + private readonly int $n, + private readonly int|null $qid + ) {} + + public function send(V4_4|V5|V5_2|V5_1|V5_3|V5_4 $bolt): void + { + $extra = ['n' => $this->n]; + if ($this->qid !== null) { + $extra['qid'] = $this->qid; + } + + $bolt->discard($extra); + } +} diff --git a/src/Bolt/Messages/GoodBye.php b/src/Bolt/Messages/GoodBye.php new file mode 100644 index 00000000..e69637dd --- /dev/null +++ b/src/Bolt/Messages/GoodBye.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Contracts\MessageInterface; + +/** + * @internal + * + * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-goodbye + */ +class GoodBye extends AbstractMessage implements MessageInterface +{ + public function send(V4_4|V5|V5_2|V5_1|V5_3|V5_4 $bolt): void + { + $bolt->goodbye(); + } +} diff --git a/src/Bolt/Messages/Hello.php b/src/Bolt/Messages/Hello.php new file mode 100644 index 00000000..b40f18cc --- /dev/null +++ b/src/Bolt/Messages/Hello.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Contracts\MessageInterface; + +/** + * @psalm-readonly + * + * @internal + * + * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-hello + */ +class Hello extends AbstractMessage implements MessageInterface +{ + /** + * @param list $routing + * @param list $notificationsDisabledCategories + * @param array{scheme: string}&array $auth + * @param array{product?: string, platform ?: string, language ?: string, language_details ?: string} $boltAgent + */ + public function __construct( + private readonly array $auth, + private readonly string|null $userAgent, + private readonly array $routing, + private readonly string|null $notificationsMinimumSeverity, + private readonly array $notificationsDisabledCategories, + private readonly array $boltAgent, + ) {} + + public function send(V4_4|V5|V5_2|V5_1|V5_3|V5_4 $bolt): void + { + $extra = [ + 'auth' => $this->auth, + ]; + + if ($this->userAgent !== null) { + $extra['user_agent'] = $this->userAgent; + } + + if ($this->routing !== []) { + $extra['routing'] = $this->routing; + } + + if ($this->notificationsMinimumSeverity !== null) { + $extra['notifications_minimum_severity'] = $this->notificationsMinimumSeverity; + } + + if ($this->notificationsDisabledCategories !== []) { + $extra['notifications_disabled_categories'] = $this->notificationsDisabledCategories; + } + + if ($this->boltAgent !== []) { + $extra['bolt_agent'] = $this->boltAgent; + } + + $bolt->hello($extra); + } +} diff --git a/src/Bolt/Messages/Logoff.php b/src/Bolt/Messages/Logoff.php new file mode 100644 index 00000000..8fb58c8d --- /dev/null +++ b/src/Bolt/Messages/Logoff.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Contracts\MessageInterface; +use LogicException; + +class Logoff extends AbstractMessage implements MessageInterface +{ + public function send(V4_4|V5|V5_2|V5_1|V5_3|V5_4 $bolt): void + { + if ($bolt instanceof V4_4 || $bolt instanceof V5) { + throw new LogicException('Cannot run logoff on bolt version 5.0 or lower. Version detected: '.$bolt->getVersion()); + } + + $bolt->logoff(); + } +} diff --git a/src/Bolt/Messages/Logon.php b/src/Bolt/Messages/Logon.php new file mode 100644 index 00000000..5f4ec4cb --- /dev/null +++ b/src/Bolt/Messages/Logon.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Contracts\MessageInterface; +use LogicException; + +class Logon extends AbstractMessage implements MessageInterface +{ + /** + * @param array{scheme: string}&array $auth + */ + public function __construct( + private readonly array $auth + ) {} + + public function send(V4_4|V5|V5_2|V5_1|V5_3|V5_4 $bolt): void + { + if ($bolt instanceof V4_4 || $bolt instanceof V5) { + throw new LogicException('Cannot run logon on bolt version 5.0 or lower. Version detected: '.$bolt->getVersion()); + } + + $bolt->logon($this->auth); + } +} diff --git a/src/Bolt/Messages/Pull.php b/src/Bolt/Messages/Pull.php new file mode 100644 index 00000000..fdd4444c --- /dev/null +++ b/src/Bolt/Messages/Pull.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Contracts\MessageInterface; + +/** + * @internal + * + * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-pull + */ +class Pull extends AbstractMessage implements MessageInterface +{ + public function __construct( + private readonly int $n, + private readonly int|null $qid + ) {} + + public function send(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $bolt): void + { + $extra = ['n' => $this->n]; + if ($this->qid !== null) { + $extra['qid'] = $this->qid; + } + + $bolt->pull($extra); + } +} diff --git a/src/Bolt/Messages/Reset.php b/src/Bolt/Messages/Reset.php new file mode 100644 index 00000000..b99a71fc --- /dev/null +++ b/src/Bolt/Messages/Reset.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Contracts\MessageInterface; + +/** + * @internal + * + * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-reset + */ +class Reset extends AbstractMessage implements MessageInterface +{ + public function send(V4_4|V5|V5_2|V5_1|V5_3|V5_4 $bolt): void + { + $bolt->reset(); + } +} diff --git a/src/Bolt/Messages/Rollback.php b/src/Bolt/Messages/Rollback.php new file mode 100644 index 00000000..a429e30c --- /dev/null +++ b/src/Bolt/Messages/Rollback.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Contracts\MessageInterface; + +class Rollback extends AbstractMessage implements MessageInterface +{ + public function send(V4_4|V5|V5_2|V5_1|V5_3|V5_4 $bolt): void + { + $bolt->rollback(); + } +} diff --git a/src/Bolt/Messages/Route.php b/src/Bolt/Messages/Route.php new file mode 100644 index 00000000..f6c86fa6 --- /dev/null +++ b/src/Bolt/Messages/Route.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Contracts\MessageInterface; + +/** + * @internal + * + * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-route + */ +class Route extends AbstractMessage implements MessageInterface +{ + public function __construct( + private readonly array $routing, + private readonly array $bookmarks, + private readonly string|null $database, + private readonly string|null $impersonatedUser, + ) {} + + public function send(V4_4|V5|V5_2|V5_1|V5_3|V5_4 $bolt): void + { + $extra = []; + + if ($this->database !== null) { + $extra['db'] = $this->database; + } + + if ($this->impersonatedUser !== null) { + $extra['imp_user'] = $this->impersonatedUser; + } + + $bolt->route($this->routing, $this->bookmarks, $extra); + } +} diff --git a/src/Bolt/Messages/Run.php b/src/Bolt/Messages/Run.php new file mode 100644 index 00000000..f694954c --- /dev/null +++ b/src/Bolt/Messages/Run.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Databags\Bookmark; +use Laudis\Neo4j\Databags\BookmarkHolder; +use Laudis\Neo4j\Enum\AccessMode; + +/** + * @psalm-readonly + * + * @internal + * + * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-run + */ +class Run extends TransactionMessage +{ + /** + * @param array $parameters + * @param array $txMetadata + * @param list $notificationsDisabledCategories + */ + public function __construct( + private readonly string $text, + private readonly array $parameters, + BookmarkHolder $bookmarks, + int|null $txTimeout, + array|null $txMetadata, + AccessMode|null $mode, + string|null $database, + string|null $impersonatedUser, + string|null $notificationsMinimumSeverity, + array $notificationsDisabledCategories + ) { + parent::__construct($bookmarks, $txTimeout, $txMetadata, $mode, $database, $impersonatedUser, $notificationsMinimumSeverity, $notificationsDisabledCategories); + } + + public function sendWithPreDecoratedExtraData(array $extra, V4_4|V5|V5_1|V5_2|V5_3|V5_4 $bolt): void + { + $bolt->run($this->text, $this->parameters, $extra); + } +} diff --git a/src/Bolt/Messages/TransactionMessage.php b/src/Bolt/Messages/TransactionMessage.php new file mode 100644 index 00000000..c70eb1f4 --- /dev/null +++ b/src/Bolt/Messages/TransactionMessage.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Bolt\Messages; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use Laudis\Neo4j\Databags\Bookmark; +use Laudis\Neo4j\Databags\BookmarkHolder; +use Laudis\Neo4j\Enum\AccessMode; + +/** + * @mixin Run + */ +abstract class TransactionMessage extends AbstractMessage +{ + /** + * @param array $txMetadata + * @param list $notificationsDisabledCategories + */ + public function __construct( + private readonly BookmarkHolder $bookmarks, + private readonly int|null $txTimeout, + private readonly array|null $txMetadata, + private readonly AccessMode|null $mode, + private readonly string|null $database, + private readonly string|null $impersonatedUser, + private readonly string|null $notificationsMinimumSeverity, + private readonly array|null $notificationsDisabledCategories + ) {} + + abstract protected function sendWithPreDecoratedExtraData(array $extra, V4_4|V5|V5_1|V5_2|V5_3|V5_4 $bolt): void; + + public function send(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $bolt): void + { + $extra = $this->basicTransactionExtra(); + + $this->sendWithPreDecoratedExtraData($extra, $bolt); + } + + private function basicTransactionExtra(): array + { + $extra = []; + $bookmarks = array_map(static fn (Bookmark $bookmark) => $bookmark->bookmark, $this->bookmarks->bookmarks); + if ($bookmarks !== []) { + $extra['bookmarks'] = $bookmarks; + } + + if ($this->mode !== null) { + $extra['mode'] = AccessMode::WRITE() === $this->mode ? 'w' : 'r'; + } + + if ($this->txTimeout !== null) { + $extra['tx_timeout'] = $this->txTimeout; + } + + if ($this->txMetadata !== []) { + $extra['tx_metadata'] = $this->txMetadata; + } + + if ($this->database !== null) { + $extra['db'] = $this->database; + } + + if ($this->impersonatedUser !== null) { + $extra['imp_user'] = $this->impersonatedUser; + } + + if ($this->notificationsMinimumSeverity !== null) { + $extra['notifications_minimum_severity'] = $this->notificationsMinimumSeverity; + } + + if ($this->notificationsDisabledCategories !== []) { + $extra['notifications_disabled_categories'] = $this->notificationsDisabledCategories; + } + + return $extra; + } +} diff --git a/src/Bolt/ProtocolFactory.php b/src/Bolt/ProtocolFactory.php index ebd0f219..ae8896e1 100644 --- a/src/Bolt/ProtocolFactory.php +++ b/src/Bolt/ProtocolFactory.php @@ -17,26 +17,28 @@ use Bolt\connection\IConnection; use Bolt\protocol\V4_4; use Bolt\protocol\V5; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; use Laudis\Neo4j\Contracts\AuthenticateInterface; use RuntimeException; -class ProtocolFactory +final class ProtocolFactory { /** - * @return array{0: V4_4|V5, 1: array{server: string, connection_id: string, hints: list}} + * @return array{0: V4_4|V5|V5_3|V5_4, 1: array{server: string, connection_id: string, hints: list}} */ public function createProtocol(IConnection $connection, AuthenticateInterface $auth, string $userAgent): array { $bolt = new Bolt($connection); - $bolt->setProtocolVersions(5, 4.4); + $bolt->setProtocolVersions(5.4, 5.3, 5, 4.4); $protocol = $bolt->build(); - if (!$protocol instanceof V4_4 && !$protocol instanceof V5) { - throw new RuntimeException('Client only supports bolt version 4.4.* and ^5.0'); + if (!($protocol instanceof V4_4 || $protocol instanceof V5 || $protocol instanceof V5_3 || $protocol instanceof V5_4)) { + throw new RuntimeException('Client only supports bolt version 4.4 and ^5.0'); } - $response = $auth->authenticateBolt($protocol, $userAgent); + $response = $auth->authenticate($protocol, $userAgent); return [$protocol, $response]; } diff --git a/src/Bolt/ProtocolViolationException.php b/src/Bolt/ProtocolViolationException.php new file mode 100644 index 00000000..5541314a --- /dev/null +++ b/src/Bolt/ProtocolViolationException.php @@ -0,0 +1,7 @@ + $patch_bolt + * @param array $hints + */ + public function __construct( + public readonly string $server, + public readonly string $connection_id, + public readonly array $patch_bolt, + public readonly array $hints + ){ + + } +} diff --git a/src/Bolt/Responses/Record.php b/src/Bolt/Responses/Record.php new file mode 100644 index 00000000..2c6290c4 --- /dev/null +++ b/src/Bolt/Responses/Record.php @@ -0,0 +1,17 @@ + $values + */ + public function __construct( + public readonly array $values + ) + { + + } +} diff --git a/src/Bolt/Responses/ResultSuccessResponse.php b/src/Bolt/Responses/ResultSuccessResponse.php new file mode 100644 index 00000000..3c55488d --- /dev/null +++ b/src/Bolt/Responses/ResultSuccessResponse.php @@ -0,0 +1,36 @@ +|null $notification + * @param array|null $plan + * @param array|null $profile + * @param array|null $stats + * @param QueryTypeEnum|null $type + */ + public function __construct( + public readonly bool $has_more, + public readonly Bookmark|null $bookmark, + public readonly string|null $db, + public readonly array|null $notification, + public readonly array|null $plan, + public readonly array|null $profile, + public readonly array|null $stats, + public readonly int|null $t_last, + public readonly int|null $t_first, + public readonly QueryTypeEnum|null $type + ) { + + } +} diff --git a/src/Bolt/Responses/RouteResponse.php b/src/Bolt/Responses/RouteResponse.php new file mode 100644 index 00000000..81e8684a --- /dev/null +++ b/src/Bolt/Responses/RouteResponse.php @@ -0,0 +1,14 @@ + $fields + */ + public function __construct(public readonly array $fields, public readonly int $t_first, public readonly int|null $qid) + { + + } +} diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php index 9c062b1d..1ff8e691 100644 --- a/src/Bolt/Session.php +++ b/src/Bolt/Session.php @@ -16,52 +16,46 @@ use Exception; use Laudis\Neo4j\Common\GeneratorHelper; use Laudis\Neo4j\Common\TransactionHelper; -use Laudis\Neo4j\Contracts\ConnectionPoolInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Contracts\SessionInterface; -use Laudis\Neo4j\Contracts\TransactionInterface; use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; use Laudis\Neo4j\Databags\Bookmark; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\SessionConfiguration; use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Enum\AccessMode; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Neo4j\Neo4jConnectionPool; -use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\ArrayList; /** * A session using bolt connections. - * - * @template ResultFormat - * - * @implements SessionInterface */ -final class Session implements SessionInterface +final class Session { /** @psalm-readonly */ private readonly BookmarkHolder $bookmarkHolder; /** - * @param ConnectionPool|Neo4jConnectionPool $pool - * @param FormatterInterface $formatter - * * @psalm-mutation-free */ public function __construct( /** @psalm-readonly */ private readonly SessionConfiguration $config, - private readonly ConnectionPoolInterface $pool, - /** - * @psalm-readonly - */ - private readonly FormatterInterface $formatter + /** @psalm-readonly */ + private readonly ConnectionPool|Neo4jConnectionPool $pool, + /** @psalm-readonly */ + private readonly SummarizedResultFormatter $formatter ) { $this->bookmarkHolder = new BookmarkHolder(Bookmark::from($config->getBookmarks())); } - public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList + /** + * @return ArrayList + */ + public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): ArrayList { $tbr = []; @@ -70,28 +64,20 @@ public function runStatements(iterable $statements, ?TransactionConfiguration $c $tbr[] = $this->beginInstantTransaction($this->config, $config)->runStatement($statement); } - return new CypherList($tbr); + return new ArrayList($tbr); } - /** - * @param iterable|null $statements - */ - public function openTransaction(iterable $statements = null, ?TransactionConfiguration $config = null): UnmanagedTransactionInterface - { - return $this->beginTransaction($statements, $this->mergeTsxConfig($config)); - } - - public function runStatement(Statement $statement, ?TransactionConfiguration $config = null) + public function runStatement(Statement $statement, ?TransactionConfiguration $config = null): SummarizedResult { return $this->runStatements([$statement], $config)->first(); } - public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null) + public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null): SummarizedResult { return $this->runStatement(new Statement($statement, $parameters), $config); } - public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) + public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null): mixed { $config = $this->mergeTsxConfig($config); @@ -101,7 +87,7 @@ public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration ); } - public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) + public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null): mixed { $config = $this->mergeTsxConfig($config); @@ -111,7 +97,7 @@ public function readTransaction(callable $tsxHandler, ?TransactionConfiguration ); } - public function transaction(callable $tsxHandler, ?TransactionConfiguration $config = null) + public function transaction(callable $tsxHandler, ?TransactionConfiguration $config = null): mixed { return $this->writeTransaction($tsxHandler, $config); } @@ -126,13 +112,10 @@ public function beginTransaction(?iterable $statements = null, ?TransactionConfi return $tsx; } - /** - * @return UnmanagedTransactionInterface - */ private function beginInstantTransaction( SessionConfiguration $config, TransactionConfiguration $tsxConfig - ): TransactionInterface { + ): BoltUnmanagedTransaction { $connection = $this->acquireConnection($tsxConfig, $config); return new BoltUnmanagedTransaction($this->config->getDatabase(), $this->formatter, $connection, $this->config, $tsxConfig, $this->bookmarkHolder); @@ -144,13 +127,12 @@ private function beginInstantTransaction( private function acquireConnection(TransactionConfiguration $config, SessionConfiguration $sessionConfig): BoltConnection { $connection = $this->pool->acquire($sessionConfig); - /** @var BoltConnection $connection */ $connection = GeneratorHelper::getReturnFromGenerator($connection); // We try and let the server do the timeout management. // Since the client should not run indefinitely, we just add the client side by two, just in case $timeout = $config->getTimeout(); - if ($timeout) { + if ($timeout !== null) { $timeout = ($timeout < 30) ? 30 : $timeout; $connection->setTimeout($timeout + 2); } diff --git a/src/Bolt/SocketConnectionFactory.php b/src/Bolt/SocketConnectionFactory.php index 482d885a..4d2316a2 100644 --- a/src/Bolt/SocketConnectionFactory.php +++ b/src/Bolt/SocketConnectionFactory.php @@ -14,6 +14,7 @@ namespace Laudis\Neo4j\Bolt; use Bolt\connection\Socket; +use Laudis\Neo4j\Common\UriConfiguration; use Laudis\Neo4j\Contracts\BasicConnectionFactoryInterface; use Laudis\Neo4j\Databags\TransactionConfiguration; diff --git a/src/Bolt/SslConfigurationFactory.php b/src/Bolt/SslConfigurationFactory.php index d4f94258..44dafb43 100644 --- a/src/Bolt/SslConfigurationFactory.php +++ b/src/Bolt/SslConfigurationFactory.php @@ -23,7 +23,7 @@ use Laudis\Neo4j\Enum\SslMode; use Psr\Http\Message\UriInterface; -class SslConfigurationFactory +final class SslConfigurationFactory { /** * @return array{0: 's'|'ssc'|'', 1: array{verify_peer?: bool, peer_name?: string, SNI_enabled?: bool, allow_self_signed?: bool}} @@ -31,14 +31,16 @@ class SslConfigurationFactory public function create(UriInterface $uri, SslConfiguration $config): array { $mode = $config->getMode(); + /** @var ''|'s'|'ssc' $sslConfig */ $sslConfig = ''; - if ($mode === SslMode::FROM_URL()) { + if ($mode === SslMode::FROM_URL) { $scheme = $uri->getScheme(); $explosion = explode('+', $scheme, 2); + /** @var ''|'s'|'ssc' $sslConfig */ $sslConfig = $explosion[1] ?? ''; - } elseif ($mode === SslMode::ENABLE()) { + } elseif ($mode === SslMode::ENABLE) { $sslConfig = 's'; - } elseif ($mode === SslMode::ENABLE_WITH_SELF_SIGNED()) { + } elseif ($mode === SslMode::ENABLE_WITH_SELF_SIGNED) { $sslConfig = 'ssc'; } @@ -46,7 +48,7 @@ public function create(UriInterface $uri, SslConfiguration $config): array return [$sslConfig, $this->enableSsl($uri->getHost(), $sslConfig, $config)]; } - return [$sslConfig, []]; + return ['', []]; } /** diff --git a/src/Bolt/StreamConnectionFactory.php b/src/Bolt/StreamConnectionFactory.php index 3d49ffee..72eff49a 100644 --- a/src/Bolt/StreamConnectionFactory.php +++ b/src/Bolt/StreamConnectionFactory.php @@ -14,6 +14,7 @@ namespace Laudis\Neo4j\Bolt; use Bolt\connection\StreamSocket; +use Laudis\Neo4j\Common\UriConfiguration; use Laudis\Neo4j\Contracts\BasicConnectionFactoryInterface; use Laudis\Neo4j\Databags\TransactionConfiguration; diff --git a/src/Bolt/SystemWideConnectionFactory.php b/src/Bolt/SystemWideConnectionFactory.php index 893be0a5..efa0c261 100644 --- a/src/Bolt/SystemWideConnectionFactory.php +++ b/src/Bolt/SystemWideConnectionFactory.php @@ -15,6 +15,7 @@ use function extension_loaded; +use Laudis\Neo4j\Common\UriConfiguration; use Laudis\Neo4j\Contracts\BasicConnectionFactoryInterface; /** @@ -28,12 +29,9 @@ class SystemWideConnectionFactory implements BasicConnectionFactoryInterface * @param SocketConnectionFactory|StreamConnectionFactory $factory */ private function __construct( - private $factory + private readonly SocketConnectionFactory|StreamConnectionFactory $factory ) {} - /** - * @psalm-suppress InvalidNullableReturnType - */ public static function getInstance(): SystemWideConnectionFactory { if (self::$instance === null) { @@ -45,7 +43,6 @@ public static function getInstance(): SystemWideConnectionFactory } } - /** @psalm-suppress NullableReturnStatement */ return self::$instance; } diff --git a/src/BoltFactory.php b/src/BoltFactory.php index 20eeb2ac..6ff23cf7 100644 --- a/src/BoltFactory.php +++ b/src/BoltFactory.php @@ -19,8 +19,8 @@ use Laudis\Neo4j\Bolt\ProtocolFactory; use Laudis\Neo4j\Bolt\SslConfigurationFactory; use Laudis\Neo4j\Bolt\SystemWideConnectionFactory; -use Laudis\Neo4j\Bolt\UriConfiguration; use Laudis\Neo4j\Common\ConnectionConfiguration; +use Laudis\Neo4j\Common\UriConfiguration; use Laudis\Neo4j\Contracts\BasicConnectionFactoryInterface; use Laudis\Neo4j\Contracts\ConnectionInterface; use Laudis\Neo4j\Databags\ConnectionRequestData; @@ -76,7 +76,7 @@ public function createConnection(ConnectionRequestData $data, SessionConfigurati return new BoltConnection($protocol, $connection, $data->getAuth(), $data->getUserAgent(), $config); } - public function canReuseConnection(ConnectionInterface $connection, ConnectionRequestData $data, SessionConfiguration $config): bool + public function canReuseConnection(BoltConnection $connection, ConnectionRequestData $data, SessionConfiguration $config): bool { $databaseInfo = $connection->getDatabaseInfo(); $database = $databaseInfo?->getName(); @@ -86,7 +86,7 @@ public function canReuseConnection(ConnectionInterface $connection, ConnectionRe $connection->getAuthentication()->toString($data->getUri()) === $data->getAuth()->toString($data->getUri()) && $connection->getEncryptionLevel() === $this->sslConfigurationFactory->create($data->getUri(), $data->getSslConfig())[0] && $connection->getUserAgent() === $data->getUserAgent() && - $connection->getAccessMode() === $config->getAccessMode() && + $connection->getAccessMode() === $config->getAccessMode() && $database === $config->getDatabase(); } diff --git a/src/Client.php b/src/Client.php index 9d862061..552c57fc 100644 --- a/src/Client.php +++ b/src/Client.php @@ -23,7 +23,6 @@ use Laudis\Neo4j\Databags\Statement; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Enum\AccessMode; -use Laudis\Neo4j\Types\CypherList; /** * A collection of drivers with methods to run queries though them. diff --git a/src/Common/DNSAddressResolver.php b/src/Common/DNSAddressResolver.php index 70025b14..6a6ac5e5 100644 --- a/src/Common/DNSAddressResolver.php +++ b/src/Common/DNSAddressResolver.php @@ -23,15 +23,16 @@ use function dns_get_record; +use Generator; use Laudis\Neo4j\Contracts\AddressResolverInterface; use Throwable; class DNSAddressResolver implements AddressResolverInterface { /** - * @return iterable + * @return Generator */ - public function getAddresses(string $host): iterable + public function getAddresses(string $host): Generator { // By using the generator pattern we make sure to call the heavy DNS IO operations // as late as possible diff --git a/src/Common/GeneratorHelper.php b/src/Common/GeneratorHelper.php index f2199fd7..80d6dad3 100644 --- a/src/Common/GeneratorHelper.php +++ b/src/Common/GeneratorHelper.php @@ -34,7 +34,7 @@ public static function getReturnFromGenerator(Generator $generator, float $timeo { $start = microtime(true); while ($generator->valid()) { - if ($timeout) { + if ($timeout !== null) { self::guardTiming($start, $timeout); } $generator->next(); diff --git a/src/Common/RoutingTable.php b/src/Common/RoutingTable.php new file mode 100644 index 00000000..ac799151 --- /dev/null +++ b/src/Common/RoutingTable.php @@ -0,0 +1,16 @@ + + */ + public function __construct( + public readonly int $ttl, + public readonly string $db, + public readonly array $servers + ) { + + } +} diff --git a/src/Common/Server.php b/src/Common/Server.php new file mode 100644 index 00000000..c4b029c3 --- /dev/null +++ b/src/Common/Server.php @@ -0,0 +1,14 @@ + - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Contracts; - -interface AddressResolverInterface -{ - /** - * Returns the addresses. - * - * @return iterable - */ - public function getAddresses(string $host): iterable; -} diff --git a/src/Contracts/AuthenticateInterface.php b/src/Contracts/AuthenticateInterface.php index 428dbd4b..dd8c64ee 100644 --- a/src/Contracts/AuthenticateInterface.php +++ b/src/Contracts/AuthenticateInterface.php @@ -15,27 +15,17 @@ use Bolt\protocol\V4_4; use Bolt\protocol\V5; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; interface AuthenticateInterface { - /** - * @psalm-mutation-free - * - * 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. * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array; - - /** - * Returns a string representation of the authentication. - */ - public function toString(UriInterface $uri): string; + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array; } diff --git a/src/Contracts/BasicConnectionFactoryInterface.php b/src/Contracts/BasicConnectionFactoryInterface.php index 2f36a1ad..7a989950 100644 --- a/src/Contracts/BasicConnectionFactoryInterface.php +++ b/src/Contracts/BasicConnectionFactoryInterface.php @@ -14,7 +14,7 @@ namespace Laudis\Neo4j\Contracts; use Laudis\Neo4j\Bolt\Connection; -use Laudis\Neo4j\Bolt\UriConfiguration; +use Laudis\Neo4j\Common\UriConfiguration; interface BasicConnectionFactoryInterface { diff --git a/src/Contracts/ClientInterface.php b/src/Contracts/ClientInterface.php deleted file mode 100644 index 5a5f1d4c..00000000 --- a/src/Contracts/ClientInterface.php +++ /dev/null @@ -1,132 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Contracts; - -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Databags\TransactionConfiguration; -use Laudis\Neo4j\Exception\Neo4jException; -use Laudis\Neo4j\Types\CypherList; - -/** - * @template ResultFormat - * - * @extends TransactionInterface - */ -interface ClientInterface extends TransactionInterface -{ - /** - * Runs a one off transaction with the provided query and parameters over the connection with the provided alias or the master alias otherwise. - * - * @param iterable $parameters - * - * @throws Neo4jException - * - * @return ResultFormat - */ - public function run(string $statement, iterable $parameters = [], ?string $alias = null); - - /** - * Runs a one off transaction with the provided statement over the connection with the provided alias or the master alias otherwise. - * - * @throws Neo4jException - * - * @return ResultFormat - */ - public function runStatement(Statement $statement, ?string $alias = null); - - /** - * Runs a one off transaction with the provided statements over the connection with the provided alias or the master alias otherwise. - * - * @param iterable $statements - * - * @throws Neo4jException - * - * @return CypherList - */ - public function runStatements(iterable $statements, ?string $alias = null): CypherList; - - /** - * Opens a transaction over the connection with the given alias if provided, the master alias otherwise. - * - * @param iterable|null $statements - * - * @throws Neo4jException - * - * @return UnmanagedTransactionInterface - */ - public function beginTransaction(?iterable $statements = null, ?string $alias = null, ?TransactionConfiguration $config = null): UnmanagedTransactionInterface; - - /** - * Gets the driver with the provided alias. Gets the default driver if no alias is provided. - * - * The driver is guaranteed to have its connectivity verified at least once during its lifetime. - * - * @return DriverInterface - */ - public function getDriver(?string $alias): DriverInterface; - - /** - * Checks to see if the Client has the driver registered with the provided alias. - */ - public function hasDriver(string $alias): bool; - - /** - * @template U - * - * @param callable(TransactionInterface):U $tsxHandler - * - * @return U - */ - public function writeTransaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null); - - /** - * @template U - * - * @param callable(TransactionInterface):U $tsxHandler - * - * @return U - */ - public function readTransaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null); - - /** - * Alias for write transaction. - * - * @template U - * - * @param callable(TransactionInterface):U $tsxHandler - * - * @return U - */ - public function transaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null); - - /** - * Checks to see if the driver can make a valid connection to the configured neo4j server. - */ - public function verifyConnectivity(?string $driver = null): bool; - - /** - * Binds a transaction to the client, so it runs all subsequent queries on the latest transaction instead of a session or the previously bound transaction. - */ - public function bindTransaction(?string $alias = null, ?TransactionConfiguration $config = null): void; - - /** - * Release a transaction from the client by committing it, so it runs all subsequent queries on a session or the previously bound transaction instead of the latest transaction. You can control the amount of transactions to be released by the depth parameter, with -1 being all transactions. - */ - public function commitBoundTransaction(?string $alias = null, int $depth = 1): void; - - /** - * Release a transaction from the client by rolling it back, so it runs all subsequent queries on a session or the previously bound transaction instead of the latest transaction. You can control the amount of transactions to be released by the depth parameter, with -1 being all transactions. - */ - public function rollbackBoundTransaction(?string $alias = null, int $depth = 1): void; -} diff --git a/src/Contracts/ConnectionInterface.php b/src/Contracts/ConnectionInterface.php deleted file mode 100644 index 3d917d99..00000000 --- a/src/Contracts/ConnectionInterface.php +++ /dev/null @@ -1,115 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Contracts; - -use Laudis\Neo4j\Databags\DatabaseInfo; -use Laudis\Neo4j\Enum\AccessMode; -use Laudis\Neo4j\Enum\ConnectionProtocol; -use Psr\Http\Message\UriInterface; - -/** - * A connection is an abstraction over a protocol used to communicate between driver and server. - * - * @template ProtocolImplementation The implementation of the protocol. - */ -interface ConnectionInterface -{ - /** - * @return ProtocolImplementation - */ - public function getImplementation(); - - /** - * Returns the authentication logic attached to this connection. - * - * @psalm-mutation-free - */ - public function getAuthentication(): AuthenticateInterface; - - /** - * Returns the agent the servers uses to identify itself. - * - * @psalm-mutation-free - */ - public function getServerAgent(): string; - - /** - * Returns the Uri used to connect to the server. - * - * @psalm-mutation-free - */ - public function getServerAddress(): UriInterface; - - /** - * Returns the version of the neo4j server. - * - * @psalm-mutation-free - */ - public function getServerVersion(): string; - - /** - * Returns the assumed server state. - */ - public function getServerState(): string; - - /** - * Returns the protocol used to connect to the server. - * - * @psalm-mutation-free - */ - public function getProtocol(): ConnectionProtocol; - - /** - * Returns the mode of access. - * - * @psalm-mutation-free - */ - public function getAccessMode(): AccessMode; - - /** - * Returns the information about the database the connection reaches. - * - * @psalm-mutation-free - */ - public function getDatabaseInfo(): ?DatabaseInfo; - - /** - * Resets the connection. - */ - public function reset(): void; - - /** - * Sets the timeout of the connection in seconds. - */ - public function setTimeout(float $timeout): void; - - /** - * Checks to see if the connection is open. - * - * @psalm-mutation-free - */ - public function isOpen(): bool; - - /** - * Encryption level can be either '', 's' or 'ssc', which stand for 'no encryption', 'full encryption' and 'self-signed encryption' respectively. - * - * @return ''|'s'|'ssc' - */ - public function getEncryptionLevel(): string; - - /** - * Returns the user agent handling this connection. - */ - public function getUserAgent(): string; -} diff --git a/src/Contracts/ConnectionPoolInterface.php b/src/Contracts/ConnectionPoolInterface.php index 56978b66..81ca9746 100644 --- a/src/Contracts/ConnectionPoolInterface.php +++ b/src/Contracts/ConnectionPoolInterface.php @@ -14,12 +14,11 @@ namespace Laudis\Neo4j\Contracts; use Generator; +use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Databags\SessionConfiguration; /** * A connection pool acts as a connection factory by managing multiple connections. - * - * @template Connection of ConnectionInterface */ interface ConnectionPoolInterface { @@ -35,7 +34,7 @@ interface ConnectionPoolInterface * int, * float, * bool, - * Connection|null + * BoltConnection|null * > */ public function acquire(SessionConfiguration $config): Generator; @@ -43,5 +42,5 @@ public function acquire(SessionConfiguration $config): Generator; /** * Releases a connection back to the pool. */ - public function release(ConnectionInterface $connection): void; + public function release(BoltConnection $connection): void; } diff --git a/src/Contracts/DriverInterface.php b/src/Contracts/DriverInterface.php index ea0ca2b3..addd6a2c 100644 --- a/src/Contracts/DriverInterface.php +++ b/src/Contracts/DriverInterface.php @@ -14,22 +14,13 @@ namespace Laudis\Neo4j\Contracts; use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Formatter\CypherList; -use Laudis\Neo4j\Formatter\CypherMap; /** * The driver creates sessions for carrying out work. - * - * @template ResultFormat - * - * @psalm-type ParsedUrl = array{host: string, pass: string|null, path: string, port: int, query: array, scheme: string, user: string|null} - * @psalm-type BasicDriver = DriverInterface>> */ interface DriverInterface { /** - * @return SessionInterface - * * @psalm-mutation-free */ public function createSession(?SessionConfiguration $config = null): SessionInterface; diff --git a/src/Contracts/FormatterInterface.php b/src/Contracts/FormatterInterface.php deleted file mode 100644 index ebaaa27b..00000000 --- a/src/Contracts/FormatterInterface.php +++ /dev/null @@ -1,116 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -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. - * - * @psalm-type CypherStats = array{ - * nodes_created: int, - * nodes_deleted: int, - * relationships_created: int, - * relationships_deleted: int, - * properties_set: int, - * labels_added: int, - * labels_removed: int, - * indexes_added: int, - * indexes_removed: int, - * constraints_added: int, - * constraints_removed: int, - * contains_updates: bool, - * contains_system_updates?: bool, - * system_updates?: int - * } - * @psalm-type BoltCypherStats = array{ - * nodes-created?: int, - * nodes-deleted?: int, - * relationships-created?: int, - * relationships-deleted?: int, - * properties-set?: int, - * labels-added?: int, - * labels-removed?: int, - * indexes-added?: int, - * indexes-removed?: int, - * constraints-added?: int, - * constraints-removed?: int, - * contains-updates?: bool, - * contains-system-updates?: bool, - * system-updates?: int - * } - * @psalm-type CypherError = array{code: string, message: string} - * @psalm-type CypherRowResponse = array{row: list>} - * @psalm-type CypherResponse = array{columns:list, data:list, stats?:CypherStats} - * @psalm-type CypherResponseSet = array{results: list, errors: list} - * @psalm-type BoltMeta = array{t_first: int, fields: list, qid ?: int} - * - * @template ResultFormat - * - * @deprecated Next major version will only use SummarizedResultFormatter - */ -interface FormatterInterface -{ - /** - * Formats the results of the bolt protocol to the unified format. - * - * @param BoltMeta $meta - * - * @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/Contracts/HasPropertiesInterface.php b/src/Contracts/HasPropertiesInterface.php deleted file mode 100644 index 7fcd58bb..00000000 --- a/src/Contracts/HasPropertiesInterface.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Contracts; - -use BadMethodCallException; -use Laudis\Neo4j\Types\CypherMap; - -/** - * Defines how an object with properties should behave. - * - * @psalm-immutable - * - * @template T - */ -interface HasPropertiesInterface -{ - /** - * Returns the properties a map. - * - * @return CypherMap - */ - public function getProperties(): CypherMap; - - /** - * @param string $name - * - * @return T - */ - public function __get($name); - - /** - * Always throws an exception as cypher objects are immutable. - * - * @param string $name - * @param T $value - * - * @throws BadMethodCallException - */ - public function __set($name, $value): void; - - /** - * Checks to see if the property exists and is set. - * - * @param string $name - */ - public function __isset($name): bool; -} diff --git a/src/Contracts/MessageInterface.php b/src/Contracts/MessageInterface.php new file mode 100644 index 00000000..113ac901 --- /dev/null +++ b/src/Contracts/MessageInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Contracts; + +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; +use JsonSerializable; +use Stringable; + +/** + * @internal + */ +interface MessageInterface extends JsonSerializable, Stringable +{ + public function send(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $bolt): void; + + public function toArray(): array; +} diff --git a/src/Contracts/PointInterface.php b/src/Contracts/PointInterface.php deleted file mode 100644 index eed1ee8d..00000000 --- a/src/Contracts/PointInterface.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Contracts; - -/** - * Defines a basic Point type in neo4j. - * - * @psalm-immutable - * - * @psalm-type Crs = 'wgs-84'|'wgs-84-3d'|'cartesian'|'cartesian-3d'; - */ -interface PointInterface -{ - /** - * Returns the x coordinate. - */ - public function getX(): float; - - /** - * Returns the y coordinate. - */ - public function getY(): float; - - /** - * Returns the Coordinates Reference System. - * - * @see https://en.wikipedia.org/wiki/Spatial_reference_system - * - * @return Crs - */ - public function getCrs(): string; - - /** - * Returns the spacial reference identifier. - * - * @see https://en.wikipedia.org/wiki/Spatial_reference_system - */ - public function getSrid(): int; -} diff --git a/src/Contracts/SessionInterface.php b/src/Contracts/SessionInterface.php deleted file mode 100644 index 57eeeb5b..00000000 --- a/src/Contracts/SessionInterface.php +++ /dev/null @@ -1,89 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Contracts; - -use Laudis\Neo4j\Databags\Bookmark; -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Databags\TransactionConfiguration; -use Laudis\Neo4j\Exception\Neo4jException; -use Laudis\Neo4j\Types\CypherList; - -/** - * A lightweight container for causally chained sequences of transactions to carry out work. - * - * @template ResultFormat - * - * @extends TransactionInterface - */ -interface SessionInterface extends TransactionInterface -{ - /** - * @param iterable $statements - * - * @throws Neo4jException - * - * @return CypherList - */ - public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList; - - /** - * @return ResultFormat - */ - public function runStatement(Statement $statement, ?TransactionConfiguration $config = null); - - /** - * @param iterable $parameters - * - * @return ResultFormat - */ - public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null); - - /** - * @psalm-param iterable|null $statements - * - * @throws Neo4jException - * - * @return UnmanagedTransactionInterface - */ - public function beginTransaction(?iterable $statements = null, ?TransactionConfiguration $config = null): UnmanagedTransactionInterface; - - /** - * @template HandlerResult - * - * @param callable(TransactionInterface):HandlerResult $tsxHandler - * - * @return HandlerResult - */ - public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null); - - /** - * @template HandlerResult - * - * @param callable(TransactionInterface):HandlerResult $tsxHandler - * - * @return HandlerResult - */ - public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null); - - /** - * @template HandlerResult - * - * @param callable(TransactionInterface):HandlerResult $tsxHandler - * - * @return HandlerResult - */ - public function transaction(callable $tsxHandler, ?TransactionConfiguration $config = null); - - public function getLastBookmark(): Bookmark; -} diff --git a/src/Contracts/TransactionInterface.php b/src/Contracts/TransactionInterface.php deleted file mode 100644 index d4c77ed0..00000000 --- a/src/Contracts/TransactionInterface.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Contracts; - -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Exception\Neo4jException; -use Laudis\Neo4j\Types\CypherList; - -/** - * Transactions are atomic units of work that may contain one or more query. - * - * @template ResultFormat - * - * @see https://neo4j.com/docs/cypher-manual/current/introduction/transactions/ - */ -interface TransactionInterface -{ - /** - * @param iterable $parameters - * - * @return ResultFormat - */ - public function run(string $statement, iterable $parameters = []); - - /** - * @return ResultFormat - */ - public function runStatement(Statement $statement); - - /** - * @param iterable $statements - * - * @throws Neo4jException - * - * @return CypherList - */ - public function runStatements(iterable $statements): CypherList; -} diff --git a/src/Contracts/UnmanagedTransactionInterface.php b/src/Contracts/UnmanagedTransactionInterface.php deleted file mode 100644 index 25f19e65..00000000 --- a/src/Contracts/UnmanagedTransactionInterface.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Contracts; - -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Types\CypherList; - -/** - * An unmanaged transaction needs to be committed or rolled back manually. - * - * @template T - * - * @extends TransactionInterface - * - * @see https://neo4j.com/docs/cypher-manual/current/introduction/transactions/ - */ -interface UnmanagedTransactionInterface extends TransactionInterface -{ - /** - * Runs the final statements provided and then commits the entire transaction. - * - * @param iterable $statements - * - * @return CypherList - */ - public function commit(iterable $statements = []): CypherList; - - /** - * Rolls back the transaction. - */ - public function rollback(): void; - - /** - * Returns whether the transaction has been rolled back. - */ - public function isRolledBack(): bool; - - /** - * Returns whether the transaction has been committed. - */ - public function isCommitted(): bool; - - /** - * Returns whether the transaction is safe to use. - */ - public function isFinished(): bool; -} diff --git a/src/Databags/Bookmark.php b/src/Databags/Bookmark.php index be774904..5036a1ab 100644 --- a/src/Databags/Bookmark.php +++ b/src/Databags/Bookmark.php @@ -13,47 +13,9 @@ namespace Laudis\Neo4j\Databags; -use function array_unique; - final class Bookmark { - /** @var list */ - private readonly array $values; - - /** - * @param list $bookmarks - */ - public function __construct(?array $bookmarks = null) + public function __construct(public readonly string $bookmark) { - $this->values = $bookmarks ?? []; - } - - public function isEmpty(): bool - { - return count($this->values) === 0; - } - - /** - * @return list - */ - public function values(): array - { - return $this->values; - } - - /** - * @param iterable|null $bookmarks - */ - public static function from(?iterable $bookmarks): self - { - $bookmarks ??= []; - $values = []; - - foreach ($bookmarks as $bookmark) { - array_push($values, ...$bookmark->values()); - $values = array_values(array_unique($values)); - } - - return new self($values); } } diff --git a/src/Databags/BookmarkHolder.php b/src/Databags/BookmarkHolder.php index 48109d19..b96da72d 100644 --- a/src/Databags/BookmarkHolder.php +++ b/src/Databags/BookmarkHolder.php @@ -15,17 +15,10 @@ final class BookmarkHolder { + /** + * @param list $bookmarks + */ public function __construct( - private Bookmark $bookmark + public readonly array $bookmarks ) {} - - public function getBookmark(): Bookmark - { - return $this->bookmark; - } - - public function setBookmark(Bookmark $bookmark): void - { - $this->bookmark = $bookmark; - } } diff --git a/src/Databags/DatabaseInfo.php b/src/Databags/DatabaseInfo.php index e3049a4a..2f88cab0 100644 --- a/src/Databags/DatabaseInfo.php +++ b/src/Databags/DatabaseInfo.php @@ -19,6 +19,8 @@ * Stores relevant information of a database. * * @psalm-immutable + * + * @extends AbstractCypherObject */ final class DatabaseInfo extends AbstractCypherObject { diff --git a/src/Databags/HttpPsrBindings.php b/src/Databags/HttpPsrBindings.php deleted file mode 100644 index 544356f0..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 $streamFactory = null, callable|RequestFactoryInterface $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 $client = null, callable|StreamFactoryInterface $streamFactory = null, callable|RequestFactoryInterface $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/Databags/Neo4jError.php b/src/Databags/Neo4jError.php index bbc7210b..d592ebed 100644 --- a/src/Databags/Neo4jError.php +++ b/src/Databags/Neo4jError.php @@ -38,8 +38,12 @@ public function __construct( */ public static function fromBoltResponse(Response $response): self { - /** @var array{code: string, message:string} $content */ - $content = $response->getContent(); + /** + * @psalm-suppress ImpurePropertyFetch + * + * @var array{code: string, message:string} $content + */ + $content = $response->content; return self::fromMessageAndCode($content['code'], $content['message']); } diff --git a/src/Databags/Pair.php b/src/Databags/Pair.php deleted file mode 100644 index 05585b39..00000000 --- a/src/Databags/Pair.php +++ /dev/null @@ -1,50 +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; - -/** - * A basic Key value Pair. - * - * @template TKey - * @template TValue - * - * @psalm-immutable - */ -final class Pair -{ - /** - * @param TKey $key - * @param TValue $value - */ - public function __construct( - private $key, - private $value - ) {} - - /** - * @return TKey - */ - public function getKey() - { - return $this->key; - } - - /** - * @return TValue - */ - public function getValue() - { - return $this->value; - } -} diff --git a/src/Databags/Plan.php b/src/Databags/Plan.php index 29b7ebc2..024653b7 100644 --- a/src/Databags/Plan.php +++ b/src/Databags/Plan.php @@ -23,6 +23,8 @@ * @see https://neo4j.com/docs/cypher-manual/current/execution-plans/ * * @psalm-immutable + * + * @extends AbstractCypherObject */ final class Plan extends AbstractCypherObject { diff --git a/src/Databags/ProfiledPlan.php b/src/Databags/ProfiledPlan.php index 5c3e6823..045cd25d 100644 --- a/src/Databags/ProfiledPlan.php +++ b/src/Databags/ProfiledPlan.php @@ -19,9 +19,11 @@ /** * A plan that has been executed. This means a lot more information is available. * - * @see \Laudis\Neo4j\Databags\Plan + * @see Plan * * @psalm-immutable + * + * @extends AbstractCypherObject */ final class ProfiledPlan extends AbstractCypherObject { diff --git a/src/Databags/ResultSummary.php b/src/Databags/ResultSummary.php index dadd95d5..6992ecda 100644 --- a/src/Databags/ResultSummary.php +++ b/src/Databags/ResultSummary.php @@ -28,6 +28,8 @@ * - information about connection environment * * @psalm-immutable + * + * @extends AbstractCypherObject */ final class ResultSummary extends AbstractCypherObject { diff --git a/src/Databags/ServerInfo.php b/src/Databags/ServerInfo.php index 9e0f58a7..7408103c 100644 --- a/src/Databags/ServerInfo.php +++ b/src/Databags/ServerInfo.php @@ -21,6 +21,8 @@ * Provides some basic information of the server where the result is obtained from. * * @psalm-immutable + * + * @extends AbstractCypherObject */ final class ServerInfo extends AbstractCypherObject { diff --git a/src/Databags/Statement.php b/src/Databags/Statement.php index ea030b66..f3cc3731 100644 --- a/src/Databags/Statement.php +++ b/src/Databags/Statement.php @@ -21,6 +21,8 @@ * @todo deprecate and create Query Object * * @psalm-immutable + * + * @extends AbstractCypherObject */ final class Statement extends AbstractCypherObject { diff --git a/src/Databags/SummarizedResult.php b/src/Databags/SummarizedResult.php index e72249a2..b2d277e9 100644 --- a/src/Databags/SummarizedResult.php +++ b/src/Databags/SummarizedResult.php @@ -14,15 +14,14 @@ namespace Laudis\Neo4j\Databags; use Generator; +use Laudis\Neo4j\Common\Value; use Laudis\Neo4j\Types\AbstractCypherSequence; use Laudis\Neo4j\Types\CypherList; /** * A result containing the values and the summary. * - * @template TValue - * - * @extends CypherList + * @extends CypherList */ final class SummarizedResult extends CypherList { diff --git a/src/Databags/SummaryCounters.php b/src/Databags/SummaryCounters.php index fbd022ac..6df81975 100644 --- a/src/Databags/SummaryCounters.php +++ b/src/Databags/SummaryCounters.php @@ -19,6 +19,8 @@ * Contains counters for various operations that a query triggered. * * @psalm-immutable + * + * @extends AbstractCypherObject */ final class SummaryCounters extends AbstractCypherObject { @@ -40,7 +42,7 @@ public function __construct( ) {} /** - * Whether or not the query contained any updates. + * Whether the query contained any updates. */ public function containsUpdates(): bool { diff --git a/src/Databags/TransactionConfiguration.php b/src/Databags/TransactionConfiguration.php index e3ed3d46..c367adae 100644 --- a/src/Databags/TransactionConfiguration.php +++ b/src/Databags/TransactionConfiguration.php @@ -28,7 +28,7 @@ final class TransactionConfiguration * @param iterable|null $metaData */ public function __construct( - private float|null $timeout = null, + private int|null $timeout = null, private iterable|null $metaData = null ) {} @@ -54,7 +54,7 @@ public static function default(): self /** * Get the configured transaction metadata. * - * @return iterable|null + * @return array|null */ public function getMetaData(): ?iterable { @@ -62,9 +62,9 @@ public function getMetaData(): ?iterable } /** - * Get the configured transaction timeout in seconds. + * Get the configured transaction timeout in ms. */ - public function getTimeout(): ?float + public function getTimeout(): ?int { return $this->timeout; } @@ -74,7 +74,7 @@ public function getTimeout(): ?float * * @param float|null $timeout timeout in seconds */ - public function withTimeout(float|null $timeout): self + public function withTimeout(int|null $timeout): self { return new self($timeout, $this->metaData); } @@ -99,11 +99,11 @@ public function merge(?TransactionConfiguration $config): self $config ??= self::default(); $metaData = $config->metaData; - if ($metaData) { + if ($metaData !== null) { $tsxConfig = $tsxConfig->withMetaData($metaData); } $timeout = $config->timeout; - if ($timeout) { + if ($timeout !== null) { $tsxConfig = $tsxConfig->withTimeout($timeout); } diff --git a/src/Enum/AccessMode.php b/src/Enum/AccessMode.php index 66da3c2e..5f0e62f6 100644 --- a/src/Enum/AccessMode.php +++ b/src/Enum/AccessMode.php @@ -16,25 +16,9 @@ use JsonSerializable; use Laudis\TypedEnum\TypedEnum; -/** - * Defines the access mode of a connection. - * - * @method static self READ() - * @method static self WRITE() - * - * @extends TypedEnum - * - * @psalm-immutable - * - * @psalm-suppress MutableDependency - */ -final class AccessMode extends TypedEnum implements JsonSerializable -{ - private const READ = 'read'; - private const WRITE = 'write'; - public function jsonSerialize(): string - { - return $this->getValue(); - } +enum AccessMode : string +{ + case READ = 'read'; + case WRITE = 'write'; } diff --git a/src/Enum/ConnectionProtocol.php b/src/Enum/ConnectionProtocol.php index ce7cd718..26ef763b 100644 --- a/src/Enum/ConnectionProtocol.php +++ b/src/Enum/ConnectionProtocol.php @@ -20,54 +20,40 @@ use Bolt\protocol\V4_3; use Bolt\protocol\V4_4; use Bolt\protocol\V5; +use Bolt\protocol\V5_1; +use Bolt\protocol\V5_2; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; use JsonSerializable; use Laudis\TypedEnum\TypedEnum; -/** - * Defines the protocol used in a connection. - * - * @method static ConnectionProtocol BOLT_V3() - * @method static ConnectionProtocol BOLT_V40() - * @method static ConnectionProtocol BOLT_V41() - * @method static ConnectionProtocol BOLT_V42() - * @method static ConnectionProtocol BOLT_V43() - * @method static ConnectionProtocol BOLT_V44() - * @method static ConnectionProtocol BOLT_V5() - * @method static ConnectionProtocol HTTP() - * - * @extends TypedEnum - * - * @psalm-immutable - * - * @psalm-suppress MutableDependency - */ -final class ConnectionProtocol extends TypedEnum implements JsonSerializable -{ - private const BOLT_V3 = '3'; - private const BOLT_V40 = '4'; - private const BOLT_V41 = '4.1'; - private const BOLT_V42 = '4.2'; - private const BOLT_V43 = '4.3'; - private const BOLT_V44 = '4.4'; - private const BOLT_V5 = '5'; - private const HTTP = 'http'; - public function isBolt(): bool - { - /** @psalm-suppress ImpureMethodCall */ - return $this !== self::HTTP(); - } +enum ConnectionProtocol: string +{ + case V3 = '3'; + case V4_0 = '4'; + case V4_1 = '4.1'; + case V4_2 = '4.2'; + case V4_3 = '4.3'; + case V4_4 = '4.4'; + case V5 = '5'; + case V5_1 = '5.1'; + case V5_2 = '5.2'; + case V5_3 = '5.3'; + case V5_4 = '5.4'; /** * @pure - * - * @psalm-suppress ImpureMethodCall */ - public static function determineBoltVersion(V3|V4|V4_1|V4_2|V4_3|V4_4|V5 $bolt): self + public static function determineBoltVersion(V3|V4|V4_1|V4_2|V4_3|V4_4|V5|V5_1|V5_2|V5_3|V5_4 $bolt): self { - $version = self::resolve($bolt->getVersion()); + foreach (self::cases() as $case) { + if ($case->name === basename(str_replace('\\', '/', get_class($bolt)))) { + return $case; + } + } - return $version[0] ?? self::BOLT_V44(); + return self::V4_4; } public function compare(ConnectionProtocol $protocol): int @@ -75,8 +61,7 @@ public function compare(ConnectionProtocol $protocol): int $x = 0; $y = 0; - /** @psalm-suppress ImpureMethodCall */ - foreach (array_values(self::getAllInstances()) as $index => $instance) { + foreach (self::cases() as $index => $instance) { if ($instance === $this) { $x = $index; } @@ -88,9 +73,4 @@ public function compare(ConnectionProtocol $protocol): int return $x - $y; } - - public function jsonSerialize(): string - { - return $this->getValue(); - } } diff --git a/src/Enum/QueryTypeEnum.php b/src/Enum/QueryTypeEnum.php index a505a6f2..d5e26f10 100644 --- a/src/Enum/QueryTypeEnum.php +++ b/src/Enum/QueryTypeEnum.php @@ -13,58 +13,10 @@ namespace Laudis\Neo4j\Enum; -use JsonSerializable; -use Laudis\Neo4j\Databags\SummaryCounters; -use Laudis\TypedEnum\TypedEnum; - -/** - * The actual type of query after is has been run. - * - * @method static self READ_ONLY() - * @method static self READ_WRITE() - * @method static self SCHEMA_WRITE() - * @method static self WRITE_ONLY() - * - * @psalm-immutable - * - * @extends TypedEnum - * - * @psalm-suppress MutableDependency - */ -final class QueryTypeEnum extends TypedEnum implements JsonSerializable, \Stringable +enum QueryTypeEnum : string { - private const READ_ONLY = 'read_only'; - private const READ_WRITE = 'read_write'; - private const SCHEMA_WRITE = 'schema_write'; - private const WRITE_ONLY = 'write_only'; - - /** - * Decide the type of the query from the provided counters. - * - * @pure - * - * @psalm-suppress ImpureMethodCall - */ - public static function fromCounters(SummaryCounters $counters): self - { - if ($counters->containsUpdates() || $counters->containsSystemUpdates()) { - return self::READ_WRITE(); - } - - if ($counters->constraintsAdded() || $counters->constraintsRemoved() || $counters->indexesAdded() || $counters->indexesRemoved()) { - return self::SCHEMA_WRITE(); - } - - return self::READ_ONLY(); - } - - public function __toString(): string - { - return $this->getValue(); - } - - public function jsonSerialize(): string - { - return $this->getValue(); - } + case READ_ONLY = 'r'; + case READ_WRITE = 'w'; + case SCHEMA_ONLY = 's'; + case WRITE_ONLY = 'rw'; } diff --git a/src/Enum/RoutingRoles.php b/src/Enum/RoutingRoles.php index 9f05a298..4fee3536 100644 --- a/src/Enum/RoutingRoles.php +++ b/src/Enum/RoutingRoles.php @@ -16,38 +16,9 @@ use JsonSerializable; use Laudis\TypedEnum\TypedEnum; -/** - * The possible routing roles. - * - * @method static RoutingRoles LEADER() - * @method static RoutingRoles FOLLOWER() - * @method static RoutingRoles ROUTE() - * - * @extends TypedEnum> - * - * @psalm-immutable - * - * @psalm-suppress MutableDependency - */ -final class RoutingRoles extends TypedEnum implements JsonSerializable +enum RoutingRoles: string { - private const LEADER = ['WRITE', 'LEADER']; - private const FOLLOWER = ['READ', 'FOLLOWER']; - private const ROUTE = ['ROUTE']; - - /** - * @psalm-suppress ImpureMethodCall - */ - public function jsonSerialize(): string - { - if ($this === self::LEADER()) { - return 'LEADER'; - } - - if ($this === self::FOLLOWER()) { - return 'FOLLOWER'; - } - - return 'ROUTE'; - } + case ROUTE = 'ROUTE'; + case READ = 'READ'; + case WRITE = 'WRITE'; } diff --git a/src/Enum/SslMode.php b/src/Enum/SslMode.php index f549db64..bba224c1 100644 --- a/src/Enum/SslMode.php +++ b/src/Enum/SslMode.php @@ -15,34 +15,13 @@ use JsonSerializable; use Laudis\TypedEnum\TypedEnum; +use Stringable; -/** - * @method static self ENABLE() - * @method static self DISABLE() - * @method static self FROM_URL() - * @method static self ENABLE_WITH_SELF_SIGNED() - * - * @extends TypedEnum - * - * @psalm-immutable - * - * @psalm-suppress MutableDependency - */ -final class SslMode extends TypedEnum implements JsonSerializable, \Stringable +enum SslMode : string { - private const ENABLE = 'enable'; - private const ENABLE_WITH_SELF_SIGNED = 'enable_with_self_signed'; - private const DISABLE = 'disable'; - private const FROM_URL = 'from_url'; - - public function __toString(): string - { - /** @noinspection MagicMethodsValidityInspection */ - return $this->getValue(); - } + case ENABLE = 'enable'; + case ENABLE_WITH_SELF_SIGNED = 'enable_with_self_signed'; + case DISABLE = 'disable'; - public function jsonSerialize(): string - { - return $this->getValue(); - } + case FROM_URL = 'from_url'; } diff --git a/src/Enum/TransactionEffect.php b/src/Enum/TransactionEffect.php deleted file mode 100644 index 9d898538..00000000 --- a/src/Enum/TransactionEffect.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Enum; - -use JsonSerializable; -use Laudis\TypedEnum\TypedEnum; - -/** - * Defines the access mode of a connection. - * - * @method static self ROLLBACK() - * @method static self NONE() - * - * @extends TypedEnum - * - * @psalm-immutable - * - * @psalm-suppress MutableDependency - */ -final class TransactionEffect extends TypedEnum implements JsonSerializable -{ - private const ROLLBACK = 'rollback'; - private const WRITE = 'none'; - - public function jsonSerialize(): string - { - return $this->getValue(); - } -} diff --git a/src/Exception/InvalidCacheArgumentException.php b/src/Exception/InvalidCacheArgumentException.php index 579da9be..42e6c3b2 100644 --- a/src/Exception/InvalidCacheArgumentException.php +++ b/src/Exception/InvalidCacheArgumentException.php @@ -16,6 +16,6 @@ use Psr\SimpleCache\InvalidArgumentException; use RuntimeException; -class InvalidCacheArgumentException extends RuntimeException implements InvalidArgumentException +final class InvalidCacheArgumentException extends RuntimeException implements InvalidArgumentException { } diff --git a/src/Exception/NoSuchRecordException.php b/src/Exception/NoSuchRecordException.php new file mode 100644 index 00000000..0c164c03 --- /dev/null +++ b/src/Exception/NoSuchRecordException.php @@ -0,0 +1,7 @@ + - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Formatter; - -use function array_key_exists; - -use Bolt\protocol\v1\structures\Path; - -use function gettype; -use function is_array; -use function is_object; -use function is_string; - -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\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use stdClass; -use UnexpectedValueException; - -/** - * Formats the result in basic CypherLists and CypherMaps. All cypher types are erased so that the map only contains scalar, null or array values. - * - * @psalm-type BasicResults = CypherList> - * - * @implements FormatterInterface - * - * @deprecated Next major version will only use SummarizedResultFormatter - */ -final class BasicFormatter implements FormatterInterface -{ - /** - * Creates a new instance of itself. - * - * @pure - */ - public static function create(): self - { - return new self(); - } - - /** - * @param array{fields: array} $meta - * - * @return CypherList> - */ - public function formatBoltResult(array $meta, BoltResult $result, BoltConnection $connection, float $runStart, float $resultAvailableAfter, Statement $statement, BookmarkHolder $holder): CypherList - { - $tbr = (new CypherList(function () use ($meta, $result) { - foreach ($result as $row) { - yield $this->formatRow($meta, $row); - } - }))->withCacheLimit($result->getFetchSize()); - - $connection->subscribeResult($tbr); - $result->addFinishedCallback(function (array $response) use ($holder) { - if (array_key_exists('bookmark', $response) && is_string($response['bookmark'])) { - $holder->setBookmark(new Bookmark([$response['bookmark']])); - } - }); - - return $tbr; - } - - /** - * @psalm-mutation-free - */ - public function formatHttpResult(ResponseInterface $response, stdClass $body, ?ConnectionInterface $connection = null, ?float $resultsAvailableAfter = null, ?float $resultsConsumedAfter = null, ?iterable $statements = null): CypherList - { - /** @var list>> */ - $tbr = []; - - /** @var stdClass $results */ - foreach ($body->results as $results) { - $tbr[] = $this->buildResult($results); - } - - return new CypherList($tbr); - } - - /** - * @return CypherList> - * - * @psalm-mutation-free - */ - private function buildResult(stdClass $result): CypherList - { - /** @var list> */ - $tbr = []; - - /** @var list $columns */ - $columns = (array) $result->columns; - /** @var stdClass $dataRow */ - foreach ($result->data as $dataRow) { - /** @var array $map */ - $map = []; - /** @var list */ - $vector = $dataRow->row; - foreach ($columns as $index => $key) { - // Removes the stdClasses from the json objects - /** @var scalar|array|null */ - $decoded = json_decode(json_encode($vector[$index], JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR); - $map[$key] = $decoded; - } - $tbr[] = new CypherMap($map); - } - - return new CypherList($tbr); - } - - /** - * @param array{fields: array} $meta - * - * @return CypherMap - */ - private function formatRow(array $meta, array $result): CypherMap - { - /** @var array $map */ - $map = []; - foreach ($meta['fields'] as $i => $column) { - $map[$column] = $this->mapValue($result[$i]); - } - - return new CypherMap($map); - } - - private function mapPath(Path $path): array - { - $relationships = $path->rels(); - $nodes = $path->nodes(); - $tbr = []; - /** - * @var mixed $node - */ - foreach ($nodes as $i => $node) { - /** @var mixed */ - $tbr[] = $node; - if (array_key_exists($i, $relationships)) { - /** @var mixed */ - $tbr[] = $relationships[$i]; - } - } - - return $tbr; - } - - /** - * @return scalar|array|null - */ - private function mapValue(mixed $value): float|array|bool|int|string|null - { - if ($value instanceof Path) { - $value = $this->mapPath($value); - } - - if (is_object($value)) { - return $this->objectToProperty($value); - } - - if ($value === null || is_scalar($value)) { - return $value; - } - - if (is_array($value)) { - return $this->remapObjectsInArray($value); - } - - throw new UnexpectedValueException('Did not expect to receive value of type: '.gettype($value)); - } - - private function objectToProperty(object $object): array - { - if ($object instanceof Path) { - return $this->mapPath($object); - } - - if (!method_exists($object, 'properties')) { - $message = 'Cannot handle objects without a properties method. Class given: '.$object::class; - throw new UnexpectedValueException($message); - } - - /** @var array */ - return $object->properties(); - } - - private function remapObjectsInArray(array $value): array - { - /** - * @psalm-var mixed $variable - */ - foreach ($value as $key => $variable) { - if (is_object($variable)) { - $value[$key] = $this->objectToProperty($variable); - } - } - - return $value; - } - - /** - * @psalm-mutation-free - */ - public function decorateRequest(RequestInterface $request, ConnectionInterface $connection): RequestInterface - { - return $request; - } - - /** - * @psalm-mutation-free - */ - public function statementConfigOverride(ConnectionInterface $connection): array - { - return [ - 'resultDataContents' => ['ROW'], - ]; - } -} diff --git a/src/Formatter/OGMFormatter.php b/src/Formatter/OGMFormatter.php deleted file mode 100644 index b2a03e08..00000000 --- a/src/Formatter/OGMFormatter.php +++ /dev/null @@ -1,175 +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; - -use function array_key_exists; - -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; -use Laudis\Neo4j\Types\CypherMap; -use Laudis\Neo4j\Types\Date; -use Laudis\Neo4j\Types\DateTime; -use Laudis\Neo4j\Types\DateTimeZoneId; -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\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. - * - * @see https://neo4j.com/docs/driver-manual/current/cypher-workflow/#driver-type-mapping - * - * @psalm-type OGMTypes = string|int|float|bool|null|Date|DateTime|Duration|LocalDateTime|LocalTime|Time|Node|Relationship|Path|Cartesian3DPoint|CartesianPoint|WGS84Point|WGS843DPoint|DateTimeZoneId|CypherList|CypherMap - * @psalm-type OGMResults = CypherList> - * - * @psalm-import-type BoltMeta from FormatterInterface - * - * @implements FormatterInterface>> - * - * @deprecated Next major version will only use SummarizedResultFormatter - */ -final class OGMFormatter implements FormatterInterface -{ - /** - * @psalm-mutation-free - */ - public function __construct( - private readonly BoltOGMTranslator $boltTranslator, - private readonly JoltHttpOGMTranslator $joltTranslator, - private readonly LegacyHttpOGMTranslator $legacyHttpTranslator - ) {} - - /** - * Creates a new instance of itself. - * - * @pure - */ - public static function create(): OGMFormatter - { - return new self(new BoltOGMTranslator(), new JoltHttpOGMTranslator(), new LegacyHttpOGMTranslator()); - } - - /** - * @param BoltMeta $meta - * - * @return CypherList> - */ - public function formatBoltResult(array $meta, BoltResult $result, BoltConnection $connection, float $runStart, float $resultAvailableAfter, Statement $statement, BookmarkHolder $holder): CypherList - { - $tbr = (new CypherList(function () use ($result, $meta) { - foreach ($result as $row) { - yield $this->formatRow($meta, $row); - } - }))->withCacheLimit($result->getFetchSize()); - - $connection->subscribeResult($tbr); - $result->addFinishedCallback(function (array $response) use ($holder) { - if (array_key_exists('bookmark', $response) && is_string($response['bookmark'])) { - $holder->setBookmark(new Bookmark([$response['bookmark']])); - } - }); - - 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 - * - * @return CypherMap - * - * @psalm-mutation-free - */ - private function formatRow(array $meta, array $result): CypherMap - { - /** @var array $map */ - $map = []; - foreach ($meta['fields'] as $i => $column) { - $map[$column] = $this->boltTranslator->mapValueToType($result[$i]); - } - - 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/BoltOGMTranslator.php b/src/Formatter/Specialised/BoltOGMTranslator.php deleted file mode 100644 index 5ad88ad0..00000000 --- a/src/Formatter/Specialised/BoltOGMTranslator.php +++ /dev/null @@ -1,307 +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 Bolt\protocol\v1\structures\Date as BoltDate; -use Bolt\protocol\v1\structures\DateTime as BoltDateTime; -use Bolt\protocol\v1\structures\DateTimeZoneId as BoltDateTimeZoneId; -use Bolt\protocol\v1\structures\Duration as BoltDuration; -use Bolt\protocol\v1\structures\LocalDateTime as BoltLocalDateTime; -use Bolt\protocol\v1\structures\LocalTime as BoltLocalTime; -use Bolt\protocol\v1\structures\Node as BoltNode; -use Bolt\protocol\v1\structures\Path as BoltPath; -use Bolt\protocol\v1\structures\Point2D as BoltPoint2D; -use Bolt\protocol\v1\structures\Point3D as BoltPoint3D; -use Bolt\protocol\v1\structures\Relationship as BoltRelationship; -use Bolt\protocol\v1\structures\Time as BoltTime; -use Bolt\protocol\v1\structures\UnboundRelationship as BoltUnboundRelationship; -use Laudis\Neo4j\Formatter\OGMFormatter; -use Laudis\Neo4j\Types\Abstract3DPoint; -use Laudis\Neo4j\Types\AbstractPoint; -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\DateTimeZoneId; -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 UnexpectedValueException; - -/** - * Translates Bolt objects to Driver Types. - * - * @psalm-import-type OGMTypes from OGMFormatter - * - * @psalm-immutable - */ -final class BoltOGMTranslator -{ - /** - * @var array - */ - private readonly array $rawToTypes; - - public function __construct() - { - $this->rawToTypes = [ - BoltNode::class => $this->makeFromBoltNode(...), - BoltDate::class => $this->makeFromBoltDate(...), - BoltDuration::class => $this->makeFromBoltDuration(...), - BoltDateTime::class => $this->makeFromBoltDateTime(...), - BoltTime::class => $this->makeFromBoltTime(...), - BoltLocalDateTime::class => $this->makeFromBoltLocalDateTime(...), - BoltLocalTime::class => $this->makeFromBoltLocalTime(...), - BoltRelationship::class => $this->makeFromBoltRelationship(...), - BoltUnboundRelationship::class => $this->makeFromBoltUnboundRelationship(...), - BoltPath::class => $this->makeFromBoltPath(...), - BoltPoint2D::class => $this->makeFromBoltPoint2D(...), - BoltPoint3D::class => $this->makeFromBoltPoint3D(...), - BoltDateTimeZoneId::class => $this->makeBoltTimezoneIdentifier(...), - 'array' => $this->mapArray(...), - 'int' => static fn (int $x): int => $x, - 'null' => static fn (): ?object => null, - 'bool' => static fn (bool $x): bool => $x, - 'string' => static fn (string $x): string => $x, - 'float' => static fn (float $x): float => $x, - ]; - } - - private function makeFromBoltNode(BoltNode $node): Node - { - /** @var array $properties */ - $properties = []; - /** - * @var string $name - * @var mixed $property - */ - foreach ($node->properties() as $name => $property) { - $properties[$name] = $this->mapValueToType($property); - } - - /** @var ?string|null $elementId */ - $elementId = null; - if ($node instanceof \Bolt\protocol\v5\structures\Node) { - $elementId = $node->element_id(); - } - /** - * @psalm-suppress MixedArgumentTypeCoercion - */ - return new Node( - $node->id(), - new CypherList($node->labels()), - new CypherMap($properties), - $elementId - ); - } - - private function makeFromBoltDate(BoltDate $date): Date - { - return new Date($date->days()); - } - - private function makeFromBoltLocalDateTime(BoltLocalDateTime $time): LocalDateTime - { - return new LocalDateTime($time->seconds(), $time->nanoseconds()); - } - - private function makeBoltTimezoneIdentifier(BoltDateTimeZoneId $time): DateTimeZoneId - { - /** @var non-empty-string $tzId */ - $tzId = $time->tz_id(); - - return new DateTimeZoneId($time->seconds(), $time->nanoseconds(), $tzId); - } - - private function makeFromBoltDuration(BoltDuration $duration): Duration - { - return new Duration( - $duration->months(), - $duration->days(), - $duration->seconds(), - $duration->nanoseconds(), - ); - } - - private function makeFromBoltDateTime(BoltDateTime $datetime): DateTime - { - return new DateTime( - $datetime->seconds(), - $datetime->nanoseconds(), - $datetime->tz_offset_seconds(), - !$datetime instanceof \Bolt\protocol\v5\structures\DateTime - ); - } - - private function makeFromBoltTime(BoltTime $time): Time - { - return new Time($time->nanoseconds(), $time->tz_offset_seconds()); - } - - private function makeFromBoltLocalTime(BoltLocalTime $time): LocalTime - { - return new LocalTime($time->nanoseconds()); - } - - private function makeFromBoltRelationship(BoltRelationship $rel): Relationship - { - /** @var array $map */ - $map = []; - /** - * @var string $key - * @var mixed $property - */ - foreach ($rel->properties() as $key => $property) { - $map[$key] = $this->mapValueToType($property); - } - - /** @var string|null $elementId */ - $elementId = null; - if ($rel instanceof \Bolt\protocol\v5\structures\Relationship) { - $elementId = $rel->element_id(); - } - - return new Relationship( - $rel->id(), - $rel->startNodeId(), - $rel->endNodeId(), - $rel->type(), - new CypherMap($map), - $elementId - ); - } - - private function makeFromBoltUnboundRelationship(BoltUnboundRelationship $rel): UnboundRelationship - { - /** @var array $map */ - $map = []; - /** - * @var string $key - * @var mixed $property - */ - foreach ($rel->properties() as $key => $property) { - $map[$key] = $this->mapValueToType($property); - } - - $elementId = null; - if ($rel instanceof \Bolt\protocol\v5\structures\UnboundRelationship) { - $elementId = $rel->element_id(); - } - - return new UnboundRelationship( - $rel->id(), - $rel->type(), - new CypherMap($map), - $elementId - ); - } - - private function makeFromBoltPoint2D(BoltPoint2d $x): AbstractPoint - { - if ($x->srid() === CartesianPoint::SRID) { - return new CartesianPoint($x->x(), $x->y()); - } elseif ($x->srid() === WGS84Point::SRID) { - return new WGS84Point($x->x(), $x->y()); - } - throw new UnexpectedValueException('An srid of '.$x->srid().' has been returned, which has not been implemented.'); - } - - private function makeFromBoltPoint3D(BoltPoint3D $x): Abstract3DPoint - { - if ($x->srid() === Cartesian3DPoint::SRID) { - return new Cartesian3DPoint($x->x(), $x->y(), $x->z()); - } elseif ($x->srid() === WGS843DPoint::SRID) { - return new WGS843DPoint($x->x(), $x->y(), $x->z()); - } - throw new UnexpectedValueException('An srid of '.$x->srid().' has been returned, which has not been implemented.'); - } - - private function makeFromBoltPath(BoltPath $path): Path - { - $nodes = []; - /** @var list $boltNodes */ - $boltNodes = $path->nodes(); - foreach ($boltNodes as $node) { - $nodes[] = $this->makeFromBoltNode($node); - } - $relationships = []; - /** @var list $rels */ - $rels = $path->rels(); - foreach ($rels as $rel) { - $relationships[] = $this->makeFromBoltUnboundRelationship($rel); - } - /** @var list $ids */ - $ids = $path->ids(); - - return new Path( - new CypherList($nodes), - new CypherList($relationships), - new CypherList($ids), - ); - } - - /** - * @return CypherList|CypherMap - */ - private function mapArray(array $value): CypherList|CypherMap - { - if (array_key_exists(0, $value)) { - /** @var array $vector */ - $vector = []; - /** @var mixed $x */ - foreach ($value as $x) { - $vector[] = $this->mapValueToType($x); - } - - return new CypherList($vector); - } - - /** @var array */ - $map = []; - /** - * @var string $key - * @var mixed $x - */ - foreach ($value as $key => $x) { - $map[$key] = $this->mapValueToType($x); - } - - return new CypherMap($map); - } - - /** - * @return OGMTypes - */ - public function mapValueToType(mixed $value) - { - $type = get_debug_type($value); - foreach ($this->rawToTypes as $class => $formatter) { - /** @psalm-suppress ArgumentTypeCoercion */ - if ($type === $class || is_a($value, $class, true)) { - return $formatter($value); - } - } - - throw new UnexpectedValueException('Cannot handle value of debug type: '.$type); - } -} 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 62d8b04c..00000000 --- a/src/Formatter/Specialised/JoltHttpOGMTranslator.php +++ /dev/null @@ -1,464 +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 - */ -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 1997f484..00000000 --- a/src/Formatter/Specialised/LegacyHttpOGMTranslator.php +++ /dev/null @@ -1,525 +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 stdClass|array|scalar|null $value - * - * @return array{0: OGMTypes, 1: HttpMetaInfo} - * - * @psalm-suppress MixedArgumentTypeCoercion - * @psalm-suppress MixedArgument - * @psalm-suppress MixedAssignment - */ - private function translateValue($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, '.')) { - [$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 - { - [$date, $time] = explode('T', $value); - $tz = null; - /** @psalm-suppress ImpureFunctionCall false positive in version php 7.4 */ - if (str_contains($time, '+')) { - [$time, $timezone] = explode('+', $time); - [$tzHours, $tzMinutes] = explode(':', $timezone); - $tz = (int) $tzHours * 60 * 60 + (int) $tzMinutes * 60; - } - [$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 - { - [$date, $time] = explode('T', $value); - [$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 deleted file mode 100644 index e3d6c824..00000000 --- a/src/Formatter/SummarizedResultFormatter.php +++ /dev/null @@ -1,246 +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; - -use function in_array; -use function is_int; - -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; -use Laudis\Neo4j\Databags\ResultSummary; -use Laudis\Neo4j\Databags\ServerInfo; -use Laudis\Neo4j\Databags\Statement; -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. - * - * @psalm-import-type CypherResponseSet from \Laudis\Neo4j\Contracts\FormatterInterface - * @psalm-import-type CypherResponse from \Laudis\Neo4j\Contracts\FormatterInterface - * @psalm-import-type BoltCypherStats from \Laudis\Neo4j\Contracts\FormatterInterface - * @psalm-import-type OGMResults from \Laudis\Neo4j\Formatter\OGMFormatter - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter - * - * @implements FormatterInterface>> - */ -final class SummarizedResultFormatter implements FormatterInterface -{ - /** - * @pure - */ - public static function create(): self - { - return new self(OGMFormatter::create()); - } - - /** - * @psalm-mutation-free - */ - 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} $response - * - * @psalm-mutation-free - */ - public function formatBoltStats(array $response): SummaryCounters - { - $stats = $response['stats'] ?? false; - if ($stats === false) { - return new SummaryCounters(); - } - - $updateCount = 0; - foreach ($stats as $key => $value) { - if (is_int($value) && !in_array($key, ['system-updates', 'contains-system-updates'])) { - $updateCount += $value; - } - } - - return 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, - $updateCount > 0, - ($stats['contains-system-updates'] ?? $stats['system-updates'] ?? 0) >= 1, - $stats['system-updates'] ?? 0 - ); - } - - public function formatBoltResult(array $meta, BoltResult $result, BoltConnection $connection, float $runStart, float $resultAvailableAfter, Statement $statement, BookmarkHolder $holder): SummarizedResult - { - /** @var ResultSummary|null $summary */ - $summary = null; - $result->addFinishedCallback(function (array $response) use ($connection, $statement, $runStart, $resultAvailableAfter, &$summary) { - /** @var BoltCypherStats $response */ - $stats = $this->formatBoltStats($response); - $resultConsumedAfter = microtime(true) - $runStart; - /** @var string */ - $db = $response['db'] ?? ''; - $summary = new ResultSummary( - $stats, - new DatabaseInfo($db), - new CypherList(), - null, - null, - $statement, - QueryTypeEnum::fromCounters($stats), - $resultAvailableAfter, - $resultConsumedAfter, - new ServerInfo( - $connection->getServerAddress(), - $connection->getProtocol(), - $connection->getServerAgent() - ) - ); - }); - - $formattedResult = $this->formatter->formatBoltResult($meta, $result, $connection, $runStart, $resultAvailableAfter, $statement, $holder); - - /** - * @psalm-suppress MixedArgument - * - * @var SummarizedResult> - */ - 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 ac2fd8a2..00000000 --- a/src/Http/HttpConnectionPool.php +++ /dev/null @@ -1,130 +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 - } -} diff --git a/src/Http/HttpDriver.php b/src/Http/HttpDriver.php deleted file mode 100644 index 4a03bd5b..00000000 --- a/src/Http/HttpDriver.php +++ /dev/null @@ -1,200 +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); - } - - if ($formatter !== null) { - return new self( - $uri, - $configuration ?? DriverConfiguration::default(), - $formatter, - $authenticate ?? Authenticate::fromUrl($uri) - ); - } - - return new self( - $uri, - $configuration ?? DriverConfiguration::default(), - OGMFormatter::create(), - $authenticate ?? Authenticate::fromUrl($uri) - ); - } - - /** - * @psalm-external-mutation-free - */ - public function createSession(?SessionConfiguration $config = null): SessionInterface - { - $factory = $this->resolvableFactory(); - $config ??= SessionConfiguration::default(); - $config = $config->merge(SessionConfiguration::fromUri($this->uri)); - $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); - }); - } -} diff --git a/src/Http/HttpHelper.php b/src/Http/HttpHelper.php deleted file mode 100644 index 3212dba5..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() >= 400) { - 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/Neo4j/Neo4jConnectionPool.php b/src/Neo4j/Neo4jConnectionPool.php index 7abe5ec4..1ca0494f 100644 --- a/src/Neo4j/Neo4jConnectionPool.php +++ b/src/Neo4j/Neo4jConnectionPool.php @@ -13,6 +13,7 @@ namespace Laudis\Neo4j\Neo4j; +use Laudis\Neo4j\Common\DNSAddressResolver; use function array_unique; use function count; @@ -22,7 +23,6 @@ use function implode; use Laudis\Neo4j\Bolt\BoltConnection; -use Laudis\Neo4j\Bolt\Connection; use Laudis\Neo4j\Bolt\ConnectionPool; use Laudis\Neo4j\BoltFactory; use Laudis\Neo4j\Common\Cache; @@ -32,7 +32,6 @@ use Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Contracts\ConnectionInterface; use Laudis\Neo4j\Contracts\ConnectionPoolInterface; -use Laudis\Neo4j\Contracts\DriverInterface; use Laudis\Neo4j\Contracts\SemaphoreInterface; use Laudis\Neo4j\Databags\ConnectionRequestData; use Laudis\Neo4j\Databags\DriverConfiguration; @@ -55,8 +54,7 @@ /** * Connection pool for with auto client-side routing. - * - * @psalm-import-type BasicDriver from DriverInterface + * * @implements ConnectionPoolInterface */ @@ -73,10 +71,10 @@ public function __construct( private readonly BoltFactory $factory, private readonly ConnectionRequestData $data, private readonly CacheInterface $cache, - private readonly AddressResolverInterface $resolver + private readonly DNSAddressResolver $resolver ) {} - public static function create(UriInterface $uri, AuthenticateInterface $auth, DriverConfiguration $conf, AddressResolverInterface $resolver, SemaphoreInterface $semaphore): self + public static function create(UriInterface $uri, AuthenticateInterface $auth, DriverConfiguration $conf, DNSAddressResolver $resolver, SemaphoreInterface $semaphore): self { return new self( $semaphore, @@ -189,7 +187,7 @@ private function routingTable(BoltConnection $connection, SessionConfiguration $ /** @var array{rt: array{servers: list, role:string}>, ttl: int}} $route */ $route = $bolt->route([], [], ['db' => $config->getDatabase()]) ->getResponse() - ->getContent(); + ->content; ['servers' => $servers, 'ttl' => $ttl] = $route['rt']; $ttl += time(); @@ -197,9 +195,9 @@ private function routingTable(BoltConnection $connection, SessionConfiguration $ return new RoutingTable($servers, $ttl); } - public function release(ConnectionInterface $connection): void + public function release(BoltConnection $connection): void { - $this->createOrGetPool($connection->getServerAddress())->release($connection); + $this->createOrGetPool($connection->getConfig()->getServerAddress())->release($connection); } private function createKey(ConnectionRequestData $data, ?SessionConfiguration $config = null): string @@ -208,7 +206,7 @@ private function createKey(ConnectionRequestData $data, ?SessionConfiguration $c $key = implode( ':', - array_filter([$data->getUserAgent(), $uri->getHost(), $config ? $config->getDatabase() : null, $uri->getPort() ?? '7687']) + array_filter([$data->getUserAgent(), $uri->getHost(), $config?->getDatabase() ?? '', $uri->getPort() ?? '7687']) ); return str_replace([ diff --git a/src/Neo4j/Neo4jDriver.php b/src/Neo4j/Neo4jDriver.php index 24cf9f71..54a21904 100644 --- a/src/Neo4j/Neo4jDriver.php +++ b/src/Neo4j/Neo4jDriver.php @@ -92,7 +92,7 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co * * @throws Exception */ - public function createSession(?SessionConfiguration $config = null): SessionInterface + public function createSession(?SessionConfiguration $config = null): Session { $config ??= SessionConfiguration::default(); $config = $config->merge(SessionConfiguration::fromUri($this->parsedUrl)); diff --git a/src/Neo4j/RoutingTable.php b/src/Neo4j/RoutingTable.php deleted file mode 100644 index 791b91c7..00000000 --- a/src/Neo4j/RoutingTable.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Neo4j; - -use function in_array; - -use Laudis\Neo4j\Enum\RoutingRoles; - -/** - * Table containing possible routes to nodes in the cluster. - * - * @psalm-immutable - */ -final class RoutingTable -{ - /** - * @param iterable, role:string}> $servers - */ - public function __construct( - private readonly iterable $servers, - private readonly int $ttl - ) {} - - /** - * Returns the time to live in seconds. - */ - public function getTtl(): int - { - return $this->ttl; - } - - /** - * Returns the routes with a given role. If no role is provided it will return all routes. - * - * @return list - */ - public function getWithRole(RoutingRoles $role = null): array - { - /** @psalm-var list $tbr */ - $tbr = []; - foreach ($this->servers as $server) { - if ($role === null || in_array($server['role'], $role->getValue(), true)) { - foreach ($server['addresses'] as $address) { - $tbr[] = $address; - } - } - } - - return array_values(array_unique($tbr)); - } -} diff --git a/src/Results/CombinedRecord.php b/src/Results/CombinedRecord.php new file mode 100644 index 00000000..d9b5e1ea --- /dev/null +++ b/src/Results/CombinedRecord.php @@ -0,0 +1,68 @@ + + * @implements Arrayable + * @implements IteratorAggregate + */ +class CombinedRecord implements ArrayAccess, Arrayable, IteratorAggregate { + + /** @var array|null */ + private array|null $combinedCache = null; + + public function __construct(private readonly RunResponse $response, private readonly Record $record) + { + + } + + public function toArray(): array + { + if ($this->combinedCache === null) { + $this->combinedCache = array_combine($this->response->fields, $this->record->values); + } + + return $this->combinedCache; + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->toArray()); + } + + public function offsetExists(mixed $offset): bool + { + return in_array($offset, $this->response->fields); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->toArray()[$offset] ?? throw new \InvalidArgumentException('Offset ' . $offset . ' does not exist.'); + } + + public function offsetSet(mixed $offset, mixed $value): void + { + throw new BadMethodCallException('Cannot modify a record'); + } + + public function offsetUnset(mixed $offset): void + { + throw new BadMethodCallException('Cannot modify a record'); + } + + public function single(): null|int|float|bool|string|array|IStructure + { + return $this->response->fields[0]; + } +} diff --git a/src/Results/ResultCursor.php b/src/Results/ResultCursor.php new file mode 100644 index 00000000..c7b9ad30 --- /dev/null +++ b/src/Results/ResultCursor.php @@ -0,0 +1,138 @@ +, CombinedRecord> + */ +final class ResultCursor implements Iterator +{ + private int $position = -1; + private CombinedRecord|null $current = null; + + private ResultSuccessResponse|null $latestResultSuccessResponse = null; + + public function __construct( + private readonly BoltConnection $connection, + private readonly Pull $pull, + private readonly RunResponse $runResponse, + ) { + } + + /** + * @return list + */ + public function keys(): array + { + return $this->runResponse->fields; + } + + /** + * Return the first record in the result, failing if there is not exactly + * one record left in the stream + *

+ * Calling this method always exhausts the result, even when {@link NoSuchRecordException} is thrown. + * + * @return Record the first and only record in the stream + * @throws NoSuchRecordException if there is not exactly one record left in the stream + */ + public function single(): CombinedRecord { + + } + + public function only(): null|int|float|array|bool|string|IStructure + { + return $this->single()->single(); + } + + /** + * Retrieve and store the entire result stream. + * This can be used if you want to iterate over the stream multiple times or to store the + * whole result for later use. + *

+ * Note that this method can only be used if you know that the query that + * yielded this result returns a finite stream. Some queries can yield + * infinite results, in which case calling this method will lead to running + * out of memory. + *

+ * Calling this method exhausts the result. + * + * @return list of all remaining immutable records + */ + public function toArray(): array { + return iterator_to_array($this); + } + + /** + * Return the result summary. + *

+ * If the records in the result is not fully consumed, then calling this method will exhausts the result. + *

+ * If you want to access unconsumed records after summary, you shall use {@link Result#list()} to buffer all records into memory before summary. + * + * @return a summary for the whole query result. + */ + public function consume(): ResultSummary + { + + } + + /** + * Determine if result is open. + *

+ * Result is considered to be open if it has not been consumed ({@link #consume()}) and its creator object (e.g. session or transaction) has not been closed + * (including committed or rolled back). + *

+ * Attempts to access data on closed result will produce {@link ResultConsumedException}. + * + * @return {@code true} if result is open and {@code false} otherwise. + */ + public function isOpen(): bool + { + + } + + public function current(): CombinedRecord + { + return $this->current; + } + + public function next(): void + { + $response = $this->connection->getResponse(); + + if ($response->signature === Signature::RECORD) { + $this->current = new CombinedRecord($this->runResponse, new Record($response->content)); + } elseif ($response->signature === Signature::SUCCESS) { + $this->latestResultSuccessResponse = new ResultSuccessResponse( + + ); + } + } + + public function key(): mixed + { + return $this->position; + } + + public function valid(): bool + { + return $this->position >= 0 && $this->current === null; + } + + public function rewind(): void + { + + } +} diff --git a/src/TypeCaster.php b/src/TypeCaster.php deleted file mode 100644 index cd0ecd41..00000000 --- a/src/TypeCaster.php +++ /dev/null @@ -1,155 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j; - -use function is_a; -use function is_iterable; -use function is_numeric; -use function is_object; -use function is_scalar; - -use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; - -use function method_exists; - -final class TypeCaster -{ - /** - * @pure - */ - public static function toString(mixed $value): ?string - { - if ($value === null || is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) { - return (string) $value; - } - - return null; - } - - /** - * @pure - */ - public static function toFloat(mixed $value): ?float - { - $value = self::toString($value); - if (is_numeric($value)) { - return (float) $value; - } - - return null; - } - - /** - * @pure - */ - public static function toInt(mixed $value): ?int - { - $value = self::toFloat($value); - if ($value !== null) { - return (int) $value; - } - - return null; - } - - /** - * @return null - * - * @pure - */ - public static function toNull() - { - return null; - } - - /** - * @pure - */ - public static function toBool(mixed $value): ?bool - { - $value = self::toInt($value); - if ($value !== null) { - return (bool) $value; - } - - return null; - } - - /** - * @template T - * - * @param class-string $class - * - * @return T|null - * - * @pure - */ - public static function toClass(mixed $value, string $class): ?object - { - if (is_a($value, $class)) { - /** @var T */ - return $value; - } - - return null; - } - - /** - * @return list - * - * @psalm-external-mutation-free - */ - public static function toArray(mixed $value): ?array - { - if (is_iterable($value)) { - $tbr = []; - /** @var mixed $x */ - foreach ($value as $x) { - /** @var mixed */ - $tbr[] = $x; - } - - return $tbr; - } - - return null; - } - - /** - * @return CypherList|null - * - * @pure - */ - public static function toCypherList(mixed $value): ?CypherList - { - if (is_iterable($value)) { - return CypherList::fromIterable($value); - } - - return null; - } - - /** - * @return CypherMap|null - */ - public static function toCypherMap(mixed $value): ?CypherMap - { - if (is_iterable($value)) { - return CypherMap::fromIterable($value); - } - - return null; - } -} diff --git a/src/Types/Abstract3DPoint.php b/src/Types/Abstract3DPoint.php deleted file mode 100644 index a2fad0e3..00000000 --- a/src/Types/Abstract3DPoint.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Bolt\protocol\IStructure; -use Bolt\protocol\v1\structures\Point3D; -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; -use Laudis\Neo4j\Contracts\PointInterface; - -/** - * A cartesian point in three-dimensional space. - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-cartesian-3d - * - * @psalm-immutable - * - * @psalm-import-type Crs from PointInterface - */ -abstract class Abstract3DPoint extends AbstractPoint implements PointInterface, BoltConvertibleInterface -{ - public function convertToBolt(): IStructure - { - return new Point3D($this->getSrid(), $this->getX(), $this->getY(), $this->getZ()); - } - - public function __construct( - float $x, - float $y, - private float $z - ) { - parent::__construct($x, $y); - } - - public function getZ(): float - { - return $this->z; - } - - /** - * @return array{x: float, y: float, z: float, srid: int, crs: Crs} - */ - public function toArray(): array - { - $tbr = parent::toArray(); - - $tbr['z'] = $this->z; - - return $tbr; - } -} diff --git a/src/Types/AbstractCypherObject.php b/src/Types/AbstractCypherObject.php deleted file mode 100644 index ae68a570..00000000 --- a/src/Types/AbstractCypherObject.php +++ /dev/null @@ -1,104 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use function array_key_exists; - -use ArrayAccess; -use ArrayIterator; -use BadMethodCallException; -use IteratorAggregate; -use JsonSerializable; -use OutOfBoundsException; - -use function sprintf; - -use Traversable; - -/** - * Abstract immutable container with basic functionality to integrate easily into the driver ecosystem. - * - * @template TKey of array-key - * @template TValue - * - * @implements ArrayAccess - * @implements IteratorAggregate - * - * @psalm-immutable - */ -abstract class AbstractCypherObject implements JsonSerializable, ArrayAccess, IteratorAggregate -{ - /** - * Represents the container as an array. - * - * @return array - */ - abstract public function toArray(): array; - - /** - * @return array - */ - public function jsonSerialize(): array - { - return $this->toArray(); - } - - /** - * @return Traversable - */ - public function getIterator(): Traversable - { - return new ArrayIterator($this->toArray()); - } - - /** - * @param TKey $offset - */ - public function offsetExists(mixed $offset): bool - { - return array_key_exists($offset, $this->toArray()); - } - - /** - * @param TKey $offset - * - * @return TValue - */ - public function offsetGet(mixed $offset): mixed - { - $serialized = $this->toArray(); - if (!array_key_exists($offset, $serialized)) { - throw new OutOfBoundsException("Offset: \"$offset\" does not exists in object of instance: ".static::class); - } - - return $serialized[$offset]; - } - - /** - * @param TKey $offset - * @param TValue $value - */ - final public function offsetSet(mixed $offset, mixed $value): void - { - throw new BadMethodCallException(sprintf('%s is immutable', static::class)); - } - - /** - * @param TKey $offset - */ - final public function offsetUnset(mixed $offset): void - { - throw new BadMethodCallException(sprintf('%s is immutable', static::class)); - } -} diff --git a/src/Types/AbstractCypherSequence.php b/src/Types/AbstractCypherSequence.php deleted file mode 100644 index f7271197..00000000 --- a/src/Types/AbstractCypherSequence.php +++ /dev/null @@ -1,563 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use function array_key_exists; -use function array_reverse; - -use ArrayAccess; -use ArrayIterator; -use BadMethodCallException; - -use function call_user_func; -use function count; - -use Countable; - -use function get_object_vars; -use function implode; - -use const INF; - -use function is_array; -use function is_callable; -use function is_numeric; -use function is_object; -use function is_string; - -use Iterator; -use JsonSerializable; - -use function method_exists; - -use OutOfBoundsException; - -use const PHP_INT_MAX; - -use function property_exists; -use function sprintf; - -use UnexpectedValueException; - -/** - * Abstract immutable sequence with basic functional methods. - * - * @template TValue - * @template TKey of array-key - * - * @implements ArrayAccess - * @implements Iterator - */ -abstract class AbstractCypherSequence implements Countable, JsonSerializable, ArrayAccess, Iterator -{ - /** @var list */ - protected array $keyCache = []; - /** @var array */ - protected array $cache = []; - private int $cacheLimit = PHP_INT_MAX; - protected int $currentPosition = 0; - protected int $generatorPosition = 0; - - /** - * @var (callable():(\Iterator))|\Iterator - */ - protected $generator; - - /** - * @template Value - * - * @param callable():(\Generator) $operation - * - * @return static - * - * @psalm-mutation-free - */ - abstract protected function withOperation($operation): self; - - /** - * Copies the sequence. - * - * @return static - * - * @psalm-mutation-free - */ - final public function copy(): self - { - return $this->withOperation(function () { - yield from $this; - }); - } - - /** - * mixed - * Returns whether the sequence is empty. - * - * @psalm-suppress UnusedForeachValue - */ - final public function isEmpty(): bool - { - /** @noinspection PhpLoopNeverIteratesInspection */ - foreach ($this as $ignored) { - return false; - } - - return true; - } - - /** - * Creates a new sequence by merging this one with the provided iterable. When the iterable is not a list, the provided values will override the existing items in case of a key collision. - * - * @template NewValue - * - * @param iterable $values - * - * @return static - * - * @psalm-mutation-free - */ - abstract public function merge(iterable $values): self; - - /** - * Checks if the sequence contains the given key. - * - * @param TKey $key - */ - final public function hasKey($key): bool - { - return $this->offsetExists($key); - } - - /** - * Checks if the sequence contains the given value. The equality check is strict. - * - * @param TValue $value - */ - final public function hasValue($value): bool - { - return $this->find($value) !== false; - } - - /** - * Creates a filtered the sequence with the provided callback. - * - * @param callable(TValue, TKey):bool $callback - * - * @return static - * - * @psalm-mutation-free - */ - final public function filter(callable $callback): self - { - return $this->withOperation(function () use ($callback) { - foreach ($this as $key => $value) { - if ($callback($value, $key)) { - yield $key => $value; - } - } - }); - } - - /** - * Maps the values of this sequence to a new one with the provided callback. - * - * @template ReturnType - * - * @param callable(TValue, TKey):ReturnType $callback - * - * @return static - * - * @psalm-mutation-free - */ - final public function map(callable $callback): self - { - return $this->withOperation(function () use ($callback) { - foreach ($this as $key => $value) { - yield $key => $callback($value, $key); - } - }); - } - - /** - * Reduces this sequence with the given callback. - * - * @template TInitial - * - * @param TInitial|null $initial - * @param callable(TInitial|null, TValue, TKey):TInitial $callback - * - * @return TInitial - */ - final public function reduce(callable $callback, $initial = null) - { - foreach ($this as $key => $value) { - $initial = $callback($initial, $value, $key); - } - - return $initial; - } - - /** - * Finds the position of the value within the sequence. - * - * @param TValue $value - * - * @return false|TKey returns the key of the value if it is found, false otherwise - */ - final public function find($value) - { - foreach ($this as $i => $x) { - if ($value === $x) { - return $i; - } - } - - return false; - } - - /** - * Creates a reversed sequence. - * - * @return static - * - * @psalm-mutation-free - */ - public function reversed(): self - { - return $this->withOperation(function () { - yield from array_reverse($this->toArray()); - }); - } - - /** - * Slices a new sequence starting from the given offset with a certain length. - * If the length is null it will slice the entire remainder starting from the offset. - * - * @return static - * - * @psalm-mutation-free - */ - public function slice(int $offset, int $length = null): self - { - return $this->withOperation(function () use ($offset, $length) { - if ($length !== 0) { - $count = -1; - $length ??= INF; - foreach ($this as $key => $value) { - ++$count; - if ($count < $offset) { - continue; - } - - yield $key => $value; - if ($count === ($offset + $length - 1)) { - break; - } - } - } - }); - } - - /** - * Creates a sorted sequence. If the comparator is null it will use natural ordering. - * - * @param (callable(TValue, TValue):int)|null $comparator - * - * @return static - * - * @psalm-mutation-free - */ - public function sorted(?callable $comparator = null): self - { - return $this->withOperation(function () use ($comparator) { - $iterable = $this->toArray(); - - if ($comparator) { - uasort($iterable, $comparator); - } else { - asort($iterable); - } - - yield from $iterable; - }); - } - - /** - * Creates a list from the arrays and objects in the sequence whose values corresponding with the provided key. - * - * @return ArrayList - * - * @psalm-mutation-free - */ - public function pluck(string $key): ArrayList - { - return new ArrayList(function () use ($key) { - foreach ($this as $value) { - if ((is_array($value) && array_key_exists($key, $value)) || ($value instanceof ArrayAccess && $value->offsetExists($key))) { - yield $value[$key]; - } elseif (is_object($value) && property_exists($value, $key)) { - yield $value->$key; - } - } - }); - } - - /** - * Uses the values found at the provided key as the key for the new Map. - * - * @return Map - * - * @psalm-mutation-free - */ - public function keyBy(string $key): Map - { - return new Map(function () use ($key) { - foreach ($this as $value) { - if (((is_array($value) && array_key_exists($key, $value)) || ($value instanceof ArrayAccess && $value->offsetExists($key))) && $this->isStringable($value[$key])) { - yield $value[$key] => $value; - } elseif (is_object($value) && property_exists($value, $key) && $this->isStringable($value->$key)) { - yield $value->$key => $value; - } else { - throw new UnexpectedValueException('Cannot convert the value to a string'); - } - } - }); - } - - /** - * Joins the values within the sequence together with the provided glue. If the glue is null, it will be an empty string. - */ - public function join(?string $glue = null): string - { - /** @psalm-suppress MixedArgumentTypeCoercion */ - return implode($glue ?? '', $this->toArray()); - } - - /** - * Iterates over the sequence and applies the callable. - * - * @param callable(TValue, TKey):void $callable - * - * @return static - */ - public function each(callable $callable): self - { - foreach ($this as $key => $value) { - $callable($value, $key); - } - - return $this; - } - - public function offsetGet(mixed $offset): mixed - { - while (!array_key_exists($offset, $this->cache) && $this->valid()) { - $this->next(); - } - - if (!array_key_exists($offset, $this->cache)) { - throw new OutOfBoundsException(sprintf('Offset: "%s" does not exists in object of instance: %s', $offset, static::class)); - } - - return $this->cache[$offset]; - } - - public function offsetSet(mixed $offset, mixed $value): void - { - throw new BadMethodCallException(sprintf('%s is immutable', static::class)); - } - - public function offsetUnset(mixed $offset): void - { - throw new BadMethodCallException(sprintf('%s is immutable', static::class)); - } - - /** - * @param TKey $offset - * - * @psalm-suppress UnusedForeachValue - */ - public function offsetExists(mixed $offset): bool - { - while (!array_key_exists($offset, $this->cache) && $this->valid()) { - $this->next(); - } - - return array_key_exists($offset, $this->cache); - } - - public function jsonSerialize(): mixed - { - return $this->toArray(); - } - - /** - * Returns the sequence as an array. - * - * @return array - */ - final public function toArray(): array - { - $this->preload(); - - return $this->cache; - } - - /** - * Returns the sequence as an array. - * - * @return array - */ - final public function toRecursiveArray(): array - { - return $this->map(static function ($x) { - if ($x instanceof self) { - return $x->toRecursiveArray(); - } - - return $x; - })->toArray(); - } - - final public function count(): int - { - return count($this->toArray()); - } - - /** - * @return TValue - */ - public function current(): mixed - { - $this->setupCache(); - - return $this->cache[$this->cacheKey()]; - } - - public function valid(): bool - { - return $this->currentPosition < $this->generatorPosition || array_key_exists($this->currentPosition, $this->keyCache) || $this->getGenerator()->valid(); - } - - public function rewind(): void - { - if ($this->currentPosition > $this->cacheLimit) { - throw new BadMethodCallException('Cannot rewind cursor: limit exceeded. In order to increase the amount of prefetched (and consequently cached) rows, increase the fetch limit in the session configuration.'); - } - - $this->currentPosition = 0; - } - - public function next(): void - { - $generator = $this->getGenerator(); - if ($this->cache === []) { - $this->setupCache(); - } elseif ($this->currentPosition === $this->generatorPosition && $generator->valid()) { - $generator->next(); - - if ($generator->valid()) { - $this->keyCache[] = $generator->key(); - $this->cache[$generator->key()] = $generator->current(); - } - ++$this->generatorPosition; - ++$this->currentPosition; - } else { - ++$this->currentPosition; - } - } - - /** - * @return TKey - */ - public function key(): mixed - { - return $this->cacheKey(); - } - - /** - * @return TKey - */ - protected function cacheKey() - { - return $this->keyCache[$this->currentPosition % max($this->cacheLimit, 1)]; - } - - /** - * @return Iterator - */ - public function getGenerator(): Iterator - { - if (is_callable($this->generator)) { - $this->generator = call_user_func($this->generator); - } - - return $this->generator; - } - - /** - * @return static - */ - public function withCacheLimit(int $cacheLimit): self - { - $tbr = $this->copy(); - $tbr->cacheLimit = $cacheLimit; - - return $tbr; - } - - private function setupCache(): void - { - $generator = $this->getGenerator(); - - if (count($this->cache) !== 0 && count($this->cache) % ($this->cacheLimit + 1) === 0) { - $this->cache = [array_key_last($this->cache) => $this->cache[array_key_last($this->cache)]]; - $this->keyCache = [$this->keyCache[array_key_last($this->keyCache)]]; - } - - if ($this->cache === [] && $generator->valid()) { - $this->cache[$generator->key()] = $generator->current(); - $this->keyCache[] = $generator->key(); - } - } - - /** - * Preload the lazy evaluation. - */ - public function preload(): void - { - while ($this->valid()) { - $this->next(); - } - } - - /** - * @psalm-mutation-free - */ - protected function isStringable(mixed $key): bool - { - return is_string($key) || is_numeric($key) || (is_object($key) && method_exists($key, '__toString')); - } - - public function __serialize(): array - { - $this->preload(); - - $tbr = get_object_vars($this); - $tbr['generator'] = new ArrayIterator($this->cache); - $tbr['currentPosition'] = 0; - $tbr['generatorPosition'] = 0; - - return $tbr; - } -} diff --git a/src/Types/AbstractPoint.php b/src/Types/AbstractPoint.php deleted file mode 100644 index 9a932bc2..00000000 --- a/src/Types/AbstractPoint.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Bolt\protocol\IStructure; -use Bolt\protocol\v1\structures\Point2D; -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; -use Laudis\Neo4j\Contracts\PointInterface; - -/** - * A cartesian point in two-dimensional space. - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-cartesian-2d - * - * @psalm-immutable - * - * @psalm-import-type Crs from PointInterface - */ -abstract class AbstractPoint extends AbstractPropertyObject implements PointInterface, BoltConvertibleInterface -{ - public function __construct( - private readonly float $x, - private readonly float $y - ) {} - - abstract public function getCrs(): string; - - abstract public function getSrid(): int; - - public function convertToBolt(): IStructure - { - return new Point2D($this->getSrid(), $this->getX(), $this->getY()); - } - - public function getX(): float - { - return $this->x; - } - - public function getY(): float - { - return $this->y; - } - - public function getProperties(): CypherMap - { - /** @psalm-suppress InvalidReturnStatement False positive */ - return new CypherMap($this); - } - - /** - * @psalm-suppress ImplementedReturnTypeMismatch False positive - * - * @return array{x: float, y: float, crs: Crs, srid: int} - */ - public function toArray(): array - { - return [ - 'x' => $this->x, - 'y' => $this->y, - 'crs' => $this->getCrs(), - 'srid' => $this->getSrid(), - ]; - } -} diff --git a/src/Types/AbstractPropertyObject.php b/src/Types/AbstractPropertyObject.php deleted file mode 100644 index 3b37ee9c..00000000 --- a/src/Types/AbstractPropertyObject.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use BadMethodCallException; -use Laudis\Neo4j\Contracts\HasPropertiesInterface; - -use function sprintf; - -/** - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter - * - * @template PropertyTypes - * @template ObjectTypes - * - * @extends AbstractCypherObject - * - * @implements HasPropertiesInterface - * - * @psalm-immutable - */ -abstract class AbstractPropertyObject extends AbstractCypherObject implements HasPropertiesInterface -{ - public function __get($name) - { - /** @psalm-suppress ImpureMethodCall */ - return $this->getProperties()->get($name); - } - - public function __set($name, $value): void - { - throw new BadMethodCallException(sprintf('%s is immutable', static::class)); - } - - public function __isset($name): bool - { - /** @psalm-suppress ImpureMethodCall */ - return $this->getProperties()->offsetExists($name); - } -} diff --git a/src/Types/ArrayList.php b/src/Types/ArrayList.php deleted file mode 100644 index 0738fa55..00000000 --- a/src/Types/ArrayList.php +++ /dev/null @@ -1,252 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use AppendIterator; -use ArrayIterator; -use Generator; - -use function is_array; -use function is_callable; -use function is_iterable; - -use Laudis\Neo4j\Exception\RuntimeTypeException; -use Laudis\Neo4j\TypeCaster; -use OutOfBoundsException; - -/** - * An immutable ordered sequence of items. - * - * @template TValue - * - * @extends AbstractCypherSequence - */ -class ArrayList extends AbstractCypherSequence -{ - /** - * @param iterable|callable():Generator $iterable - * - * @psalm-mutation-free - */ - public function __construct($iterable = []) - { - if (is_array($iterable)) { - $iterable = new ArrayIterator($iterable); - } - - $this->generator = static function () use ($iterable): Generator { - $i = 0; - /** @var Generator $it */ - $it = is_callable($iterable) ? $iterable() : $iterable; - foreach ($it as $value) { - yield $i => $value; - ++$i; - } - }; - } - - /** - * @template Value - * - * @param callable():(\Generator) $operation - * - * @return static - * - * @psalm-mutation-free - */ - protected function withOperation($operation): AbstractCypherSequence - { - /** @psalm-suppress UnsafeInstantiation */ - return new static($operation); - } - - /** - * Returns the first element in the sequence. - * - * @return TValue - */ - public function first() - { - foreach ($this as $value) { - return $value; - } - - throw new OutOfBoundsException('Cannot grab first element of an empty list'); - } - - /** - * Returns the last element in the sequence. - * - * @return TValue - */ - public function last() - { - if ($this->isEmpty()) { - throw new OutOfBoundsException('Cannot grab last element of an empty list'); - } - - $array = $this->toArray(); - - return $array[count($array) - 1]; - } - - /** - * @template NewValue - * - * @param iterable $values - * - * @return static - * - * @psalm-mutation-free - */ - public function merge($values): ArrayList - { - return $this->withOperation(function () use ($values): Generator { - $iterator = new AppendIterator(); - - $iterator->append($this); - $iterator->append(new self($values)); - - yield from $iterator; - }); - } - - /** - * Gets the nth element in the list. - * - * @throws OutOfBoundsException - * - * @return TValue - */ - public function get(int $key) - { - return $this->offsetGet($key); - } - - public function getAsString(int $key): string - { - $value = $this->get($key); - $tbr = TypeCaster::toString($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'string'); - } - - return $tbr; - } - - public function getAsInt(int $key): int - { - $value = $this->get($key); - $tbr = TypeCaster::toInt($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'int'); - } - - return $tbr; - } - - public function getAsFloat(int $key): float - { - $value = $this->get($key); - $tbr = TypeCaster::toFloat($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'float'); - } - - return $tbr; - } - - public function getAsBool(int $key): bool - { - $value = $this->get($key); - $tbr = TypeCaster::toBool($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'bool'); - } - - return $tbr; - } - - /** - * @return null - */ - public function getAsNull(int $key) - { - /** @psalm-suppress UnusedMethodCall */ - $this->get($key); - - return TypeCaster::toNull(); - } - - /** - * @template U - * - * @param class-string $class - * - * @return U - */ - public function getAsObject(int $key, string $class): object - { - $value = $this->get($key); - $tbr = TypeCaster::toClass($value, $class); - if ($tbr === null) { - throw new RuntimeTypeException($value, $class); - } - - return $tbr; - } - - /** - * @return Map - */ - public function getAsMap(int $key): Map - { - $value = $this->get($key); - if (!is_iterable($value)) { - throw new RuntimeTypeException($value, Map::class); - } - - /** @psalm-suppress MixedArgumentTypeCoercion */ - return new Map($value); - } - - /** - * @return ArrayList - */ - public function getAsArrayList(int $key): ArrayList - { - $value = $this->get($key); - if (!is_iterable($value)) { - throw new RuntimeTypeException($value, self::class); - } - - /** @psalm-suppress MixedArgumentTypeCoercion */ - return new ArrayList($value); - } - - /** - * @template Value - * - * @param iterable $iterable - * - * @return static - * - * @pure - */ - public static function fromIterable(iterable $iterable): ArrayList - { - /** @psalm-suppress UnsafeInstantiation */ - return new static($iterable); - } -} diff --git a/src/Types/Cartesian3DPoint.php b/src/Types/Cartesian3DPoint.php deleted file mode 100644 index b8294093..00000000 --- a/src/Types/Cartesian3DPoint.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; -use Laudis\Neo4j\Contracts\PointInterface; - -/** - * A cartesian point in three dimensional space. - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-cartesian-3d - * - * @psalm-immutable - * - * @psalm-import-type Crs from \Laudis\Neo4j\Contracts\PointInterface - */ -final class Cartesian3DPoint extends Abstract3DPoint implements PointInterface, BoltConvertibleInterface -{ - public const SRID = 9157; - public const CRS = 'cartesian-3d'; - - public function getSrid(): int - { - return self::SRID; - } - - public function getCrs(): string - { - return self::CRS; - } -} diff --git a/src/Types/CartesianPoint.php b/src/Types/CartesianPoint.php deleted file mode 100644 index 2700f381..00000000 --- a/src/Types/CartesianPoint.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; -use Laudis\Neo4j\Contracts\PointInterface; - -/** - * A cartesian point in two dimensional space. - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-cartesian-2d - * - * @psalm-immutable - * - * @psalm-import-type Crs from \Laudis\Neo4j\Contracts\PointInterface - */ -final class CartesianPoint extends AbstractPoint implements PointInterface, BoltConvertibleInterface -{ - /** @var Crs */ - public const CRS = 'cartesian'; - public const SRID = 7203; - - public function getCrs(): string - { - return self::CRS; - } - - public function getSrid(): int - { - return self::SRID; - } -} diff --git a/src/Types/CypherList.php b/src/Types/CypherList.php deleted file mode 100644 index 29864c7f..00000000 --- a/src/Types/CypherList.php +++ /dev/null @@ -1,120 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Laudis\Neo4j\Exception\RuntimeTypeException; -use Laudis\Neo4j\TypeCaster; - -/** - * An immutable ordered sequence of items. - * - * @template TValue - * - * @extends ArrayList - */ -class CypherList extends ArrayList -{ - /** - * @return CypherMap - */ - public function getAsCypherMap(int $key): CypherMap - { - $value = $this->get($key); - $tbr = TypeCaster::toCypherMap($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, CypherMap::class); - } - - return $tbr; - } - - /** - * @return CypherList - */ - public function getAsCypherList(int $key): CypherList - { - $value = $this->get($key); - $tbr = TypeCaster::toCypherList($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, CypherList::class); - } - - return $tbr; - } - - public function getAsDate(int $key): Date - { - return $this->getAsObject($key, Date::class); - } - - public function getAsDateTime(int $key): DateTime - { - return $this->getAsObject($key, DateTime::class); - } - - public function getAsDuration(int $key): Duration - { - return $this->getAsObject($key, Duration::class); - } - - public function getAsLocalDateTime(int $key): LocalDateTime - { - return $this->getAsObject($key, LocalDateTime::class); - } - - public function getAsLocalTime(int $key): LocalTime - { - return $this->getAsObject($key, LocalTime::class); - } - - public function getAsTime(int $key): Time - { - return $this->getAsObject($key, Time::class); - } - - public function getAsNode(int $key): Node - { - return $this->getAsObject($key, Node::class); - } - - public function getAsRelationship(int $key): Relationship - { - return $this->getAsObject($key, Relationship::class); - } - - public function getAsPath(int $key): Path - { - return $this->getAsObject($key, Path::class); - } - - public function getAsCartesian3DPoint(int $key): Cartesian3DPoint - { - return $this->getAsObject($key, Cartesian3DPoint::class); - } - - public function getAsCartesianPoint(int $key): CartesianPoint - { - return $this->getAsObject($key, CartesianPoint::class); - } - - public function getAsWGS84Point(int $key): WGS84Point - { - return $this->getAsObject($key, WGS84Point::class); - } - - public function getAsWGS843DPoint(int $key): WGS843DPoint - { - return $this->getAsObject($key, WGS843DPoint::class); - } -} diff --git a/src/Types/CypherMap.php b/src/Types/CypherMap.php deleted file mode 100644 index 2a78832d..00000000 --- a/src/Types/CypherMap.php +++ /dev/null @@ -1,214 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use function func_num_args; - -use Laudis\Neo4j\Exception\RuntimeTypeException; -use Laudis\Neo4j\TypeCaster; - -/** - * An immutable ordered map of items. - * - * @template TValue - * - * @extends Map - */ -final class CypherMap extends Map -{ - /** - * @return CypherMap - */ - public function getAsCypherMap(string $key, mixed $default = null): CypherMap - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - $tbr = TypeCaster::toCypherMap($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, self::class); - } - - return $tbr; - } - - /** - * @return CypherList - */ - public function getAsCypherList(string $key, mixed $default = null): CypherList - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - $tbr = TypeCaster::toCypherList($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, CypherList::class); - } - - return $tbr; - } - - public function getAsDate(string $key, mixed $default = null): Date - { - if (func_num_args() === 1) { - return $this->getAsObject($key, Date::class); - } - - return $this->getAsObject($key, Date::class, $default); - } - - public function getAsDateTime(string $key, mixed $default = null): DateTime - { - if (func_num_args() === 1) { - return $this->getAsObject($key, DateTime::class); - } - - return $this->getAsObject($key, DateTime::class, $default); - } - - public function getAsDuration(string $key, mixed $default = null): Duration - { - if (func_num_args() === 1) { - return $this->getAsObject($key, Duration::class); - } - - return $this->getAsObject($key, Duration::class, $default); - } - - public function getAsLocalDateTime(string $key, mixed $default = null): LocalDateTime - { - if (func_num_args() === 1) { - return $this->getAsObject($key, LocalDateTime::class); - } - - return $this->getAsObject($key, LocalDateTime::class, $default); - } - - public function getAsLocalTime(string $key, mixed $default = null): LocalTime - { - if (func_num_args() === 1) { - return $this->getAsObject($key, LocalTime::class); - } - - return $this->getAsObject($key, LocalTime::class, $default); - } - - public function getAsTime(string $key, mixed $default = null): Time - { - if (func_num_args() === 1) { - return $this->getAsObject($key, Time::class); - } - - return $this->getAsObject($key, Time::class, $default); - } - - public function getAsNode(string $key, mixed $default = null): Node - { - if (func_num_args() === 1) { - return $this->getAsObject($key, Node::class); - } - - return $this->getAsObject($key, Node::class, $default); - } - - public function getAsRelationship(string $key, mixed $default = null): Relationship - { - if (func_num_args() === 1) { - return $this->getAsObject($key, Relationship::class); - } - - return $this->getAsObject($key, Relationship::class, $default); - } - - public function getAsPath(string $key, mixed $default = null): Path - { - if (func_num_args() === 1) { - return $this->getAsObject($key, Path::class); - } - - return $this->getAsObject($key, Path::class, $default); - } - - public function getAsCartesian3DPoint(string $key, mixed $default = null): Cartesian3DPoint - { - if (func_num_args() === 1) { - return $this->getAsObject($key, Cartesian3DPoint::class); - } - - return $this->getAsObject($key, Cartesian3DPoint::class, $default); - } - - public function getAsCartesianPoint(string $key, mixed $default = null): CartesianPoint - { - if (func_num_args() === 1) { - return $this->getAsObject($key, CartesianPoint::class); - } - - return $this->getAsObject($key, CartesianPoint::class, $default); - } - - public function getAsWGS84Point(string $key, mixed $default = null): WGS84Point - { - if (func_num_args() === 1) { - return $this->getAsObject($key, WGS84Point::class); - } - - return $this->getAsObject($key, WGS84Point::class, $default); - } - - public function getAsWGS843DPoint(string $key, mixed $default = null): WGS843DPoint - { - if (func_num_args() === 1) { - return $this->getAsObject($key, WGS843DPoint::class); - } - - return $this->getAsObject($key, WGS843DPoint::class, $default); - } - - /** - * @template Value - * - * @param iterable $iterable - * - * @return self - * - * @pure - */ - public static function fromIterable(iterable $iterable): CypherMap - { - return new self($iterable); - } - - /** - * @psalm-mutation-free - */ - public function pluck(string $key): CypherList - { - return CypherList::fromIterable(parent::pluck($key)); - } - - /** - * @psalm-mutation-free - */ - public function keyBy(string $key): CypherMap - { - return CypherMap::fromIterable(parent::keyBy($key)); - } -} diff --git a/src/Types/Date.php b/src/Types/Date.php deleted file mode 100644 index 070d5ec8..00000000 --- a/src/Types/Date.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Bolt\protocol\IStructure; -use DateTimeImmutable; -use Exception; -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; -use UnexpectedValueException; - -/** - * A date represented by days since unix epoch. - * - * @psalm-immutable - * - * @extends AbstractPropertyObject - * - * @psalm-suppress TypeDoesNotContainType - */ -final class Date extends AbstractPropertyObject implements BoltConvertibleInterface -{ - public function __construct( - private readonly int $days - ) {} - - /** - * The amount of days since unix epoch. - */ - public function getDays(): int - { - return $this->days; - } - - /** - * Casts to an immutable date time. - * - * @throws Exception - */ - public function toDateTime(): DateTimeImmutable - { - $dateTimeImmutable = (new DateTimeImmutable('@0'))->modify(sprintf('+%s days', $this->days)); - - if ($dateTimeImmutable === false) { - throw new UnexpectedValueException('Expected DateTimeImmutable'); - } - - return $dateTimeImmutable; - } - - public function getProperties(): CypherMap - { - return new CypherMap($this); - } - - public function toArray(): array - { - return ['days' => $this->days]; - } - - public function convertToBolt(): IStructure - { - return new \Bolt\protocol\v1\structures\Date($this->getDays()); - } -} diff --git a/src/Types/DateTime.php b/src/Types/DateTime.php deleted file mode 100644 index f4a1b421..00000000 --- a/src/Types/DateTime.php +++ /dev/null @@ -1,122 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Bolt\protocol\IStructure; -use DateTimeImmutable; -use DateTimeZone; -use Exception; -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; - -use function sprintf; - -/** - * A date represented by seconds and nanoseconds since unix epoch, enriched with a timezone offset in seconds. - * - * @psalm-immutable - * - * @extends AbstractPropertyObject - */ -final class DateTime extends AbstractPropertyObject implements BoltConvertibleInterface -{ - public function __construct( - private readonly int $seconds, - private readonly int $nanoseconds, - private readonly int $tzOffsetSeconds, - private readonly bool $legacy - ) {} - - /** - * Returns whether this DateTime Type follows conventions up until Neo4j version 4. - */ - public function isLegacy(): bool - { - return $this->legacy; - } - - /** - * Returns the amount of seconds since unix epoch. - */ - public function getSeconds(): int - { - return $this->seconds; - } - - /** - * Returns the amount of nanoseconds after the seconds have passed. - */ - public function getNanoseconds(): int - { - return $this->nanoseconds; - } - - /** - * Returns the timezone offset in seconds. - */ - public function getTimeZoneOffsetSeconds(): int - { - return $this->tzOffsetSeconds; - } - - /** - * Casts to an immutable date time. - * - * @throws Exception - */ - public function toDateTime(): DateTimeImmutable - { - $dateTime = new DateTimeImmutable(sprintf('@%s', $this->getSeconds())); - $dateTime = $dateTime->modify(sprintf('+%s microseconds', $this->nanoseconds / 1000)); - /** @psalm-suppress PossiblyFalseReference */ - $dateTime = $dateTime->setTimezone(new DateTimeZone(sprintf("%+'05d", $this->getTimeZoneOffsetSeconds() / 3600 * 100))); - - if ($this->legacy) { - /** - * @psalm-suppress FalsableReturnStatement - * - * @var DateTimeImmutable - */ - return $dateTime->modify(sprintf('-%s seconds', $this->getTimeZoneOffsetSeconds())); - } - - /** @var DateTimeImmutable */ - return $dateTime; - } - - /** - * @return array{seconds: int, nanoseconds: int, tzOffsetSeconds: int} - */ - public function toArray(): array - { - return [ - 'seconds' => $this->seconds, - 'nanoseconds' => $this->nanoseconds, - 'tzOffsetSeconds' => $this->tzOffsetSeconds, - ]; - } - - public function getProperties(): CypherMap - { - return new CypherMap($this); - } - - public function convertToBolt(): IStructure - { - if ($this->legacy) { - return new \Bolt\protocol\v1\structures\DateTime($this->getSeconds(), $this->getNanoseconds(), $this->getTimeZoneOffsetSeconds()); - } - - return new \Bolt\protocol\v5\structures\DateTime($this->getSeconds(), $this->getNanoseconds(), $this->getTimeZoneOffsetSeconds()); - } -} diff --git a/src/Types/DateTimeZoneId.php b/src/Types/DateTimeZoneId.php deleted file mode 100644 index b734aebd..00000000 --- a/src/Types/DateTimeZoneId.php +++ /dev/null @@ -1,111 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Bolt\protocol\IStructure; -use DateTimeImmutable; -use DateTimeZone; -use Exception; -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; - -use function sprintf; - -use UnexpectedValueException; - -/** - * A date represented by seconds and nanoseconds since unix epoch, enriched with a timezone identifier. - * - * @psalm-immutable - * - * @extends AbstractPropertyObject - * - * @psalm-suppress TypeDoesNotContainType - */ -final class DateTimeZoneId extends AbstractPropertyObject implements BoltConvertibleInterface -{ - /** - * @param non-empty-string $tzId - */ - public function __construct( - private readonly int $seconds, - private readonly int $nanoseconds, - private readonly string $tzId - ) {} - - /** - * Returns the amount of seconds since unix epoch. - */ - public function getSeconds(): int - { - return $this->seconds; - } - - /** - * Returns the amount of nanoseconds after the seconds have passed. - */ - public function getNanoseconds(): int - { - return $this->nanoseconds; - } - - /** - * Returns the timezone identifier. - */ - public function getTimezoneIdentifier(): string - { - return $this->tzId; - } - - /** - * Casts to an immutable date time. - * - * @throws Exception - */ - public function toDateTime(): DateTimeImmutable - { - $dateTimeImmutable = (new DateTimeImmutable(sprintf('@%s', $this->getSeconds()))) - ->modify(sprintf('+%s microseconds', $this->nanoseconds / 1000)); - - if ($dateTimeImmutable === false) { - throw new UnexpectedValueException('Expected DateTimeImmutable'); - } - - return $dateTimeImmutable->setTimezone(new DateTimeZone($this->tzId)); - } - - /** - * @return array{seconds: int, nanoseconds: int, tzId: string} - */ - public function toArray(): array - { - return [ - 'seconds' => $this->seconds, - 'nanoseconds' => $this->nanoseconds, - 'tzId' => $this->tzId, - ]; - } - - /** - * @return CypherMap - */ - public function getProperties(): CypherMap - { - return new CypherMap($this); - } - - public function convertToBolt(): IStructure - { - return new \Bolt\protocol\v1\structures\DateTimeZoneId($this->getSeconds(), $this->getNanoseconds(), $this->getTimezoneIdentifier()); - } -} diff --git a/src/Types/Duration.php b/src/Types/Duration.php deleted file mode 100644 index cf0dd293..00000000 --- a/src/Types/Duration.php +++ /dev/null @@ -1,106 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Bolt\protocol\IStructure; -use DateInterval; -use Exception; -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; - -/** - * A temporal range represented in months, days, seconds and nanoseconds. - * - * @psalm-immutable - * - * @extends AbstractPropertyObject - */ -final class Duration extends AbstractPropertyObject implements BoltConvertibleInterface -{ - public function __construct( - private readonly int $months, - private readonly int $days, - private readonly int $seconds, - private readonly int $nanoseconds - ) {} - - /** - * The amount of months in the duration. - */ - public function getMonths(): int - { - return $this->months; - } - - /** - * The amount of days in the duration after the months have passed. - */ - public function getDays(): int - { - return $this->days; - } - - /** - * The amount of seconds in the duration after the days have passed. - */ - public function getSeconds(): int - { - return $this->seconds; - } - - /** - * The amount of nanoseconds in the duration after the seconds have passed. - */ - public function getNanoseconds(): int - { - return $this->nanoseconds; - } - - /** - * Casts to a DateInterval object. - * - * @throws Exception - */ - public function toDateInterval(): DateInterval - { - return new DateInterval(sprintf('P%dM%dDT%dS', $this->months, $this->days, $this->seconds)); - } - - /** - * @return array{months: int, days: int, seconds: int, nanoseconds: int} - */ - public function toArray(): array - { - return [ - 'months' => $this->months, - 'days' => $this->days, - 'seconds' => $this->seconds, - 'nanoseconds' => $this->nanoseconds, - ]; - } - - public function getProperties(): CypherMap - { - return new CypherMap($this); - } - - public function convertToBolt(): IStructure - { - return new \Bolt\protocol\v1\structures\Duration( - $this->getMonths(), - $this->getDays(), - $this->getSeconds(), - $this->getNanoseconds() - ); - } -} diff --git a/src/Types/LocalDateTime.php b/src/Types/LocalDateTime.php deleted file mode 100644 index 114d09b6..00000000 --- a/src/Types/LocalDateTime.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Bolt\protocol\IStructure; -use DateTimeImmutable; -use Exception; -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; - -use function sprintf; - -use UnexpectedValueException; - -/** - * A date time represented in seconds and nanoseconds since the unix epoch. - * - * @psalm-immutable - * - * @extends AbstractPropertyObject - * - * @psalm-suppress TypeDoesNotContainType - */ -final class LocalDateTime extends AbstractPropertyObject implements BoltConvertibleInterface -{ - public function __construct( - private readonly int $seconds, - private readonly int $nanoseconds - ) {} - - /** - * The amount of seconds since the unix epoch. - */ - public function getSeconds(): int - { - return $this->seconds; - } - - /** - * The amount of nanoseconds after the seconds have passed. - */ - public function getNanoseconds(): int - { - return $this->nanoseconds; - } - - /** - * @throws Exception - */ - public function toDateTime(): DateTimeImmutable - { - $dateTimeImmutable = (new DateTimeImmutable(sprintf('@%s', $this->getSeconds())))->modify(sprintf('+%s microseconds', $this->nanoseconds / 1000)); - - if ($dateTimeImmutable === false) { - throw new UnexpectedValueException('Expected DateTimeImmutable'); - } - - return $dateTimeImmutable; - } - - /** - * @return array{seconds: int, nanoseconds: int} - */ - public function toArray(): array - { - return [ - 'seconds' => $this->seconds, - 'nanoseconds' => $this->nanoseconds, - ]; - } - - public function getProperties(): CypherMap - { - return new CypherMap($this); - } - - public function convertToBolt(): IStructure - { - return new \Bolt\protocol\v1\structures\LocalDateTime($this->getSeconds(), $this->getNanoseconds()); - } -} diff --git a/src/Types/LocalTime.php b/src/Types/LocalTime.php deleted file mode 100644 index f53b8644..00000000 --- a/src/Types/LocalTime.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Bolt\protocol\IStructure; -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; - -/** - * The time of day represented in nanoseconds. - * - * @psalm-immutable - * - * @extends AbstractPropertyObject - */ -final class LocalTime extends AbstractPropertyObject implements BoltConvertibleInterface -{ - public function __construct( - private readonly int $nanoseconds - ) {} - - /** - * The nanoseconds that have passed since midnight. - */ - public function getNanoseconds(): int - { - return $this->nanoseconds; - } - - /** - * @return array{nanoseconds: int} - */ - public function toArray(): array - { - return ['nanoseconds' => $this->nanoseconds]; - } - - public function getProperties(): CypherMap - { - return new CypherMap($this); - } - - public function convertToBolt(): IStructure - { - return new \Bolt\protocol\v1\structures\LocalTime($this->getNanoseconds()); - } -} diff --git a/src/Types/Map.php b/src/Types/Map.php deleted file mode 100644 index d03e432a..00000000 --- a/src/Types/Map.php +++ /dev/null @@ -1,508 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use function array_key_exists; -use function array_key_last; - -use ArrayIterator; - -use function count; -use function func_num_args; - -use Generator; - -use function is_array; -use function is_callable; -use function is_iterable; - -use Laudis\Neo4j\Databags\Pair; -use Laudis\Neo4j\Exception\RuntimeTypeException; -use Laudis\Neo4j\TypeCaster; -use OutOfBoundsException; - -use function sprintf; - -use stdClass; - -/** - * An immutable ordered map of items. - * - * @template TValue - * - * @extends AbstractCypherSequence - */ -class Map extends AbstractCypherSequence -{ - /** - * @param iterable|callable():Generator $iterable - * - * @psalm-mutation-free - */ - public function __construct($iterable = []) - { - if (is_array($iterable)) { - $i = 0; - foreach ($iterable as $key => $value) { - if (!$this->isStringable($key)) { - $key = (string) $i; - } - /** @var string $key */ - $this->keyCache[] = $key; - /** @var TValue $value */ - $this->cache[$key] = $value; - ++$i; - } - /** @var ArrayIterator */ - $it = new ArrayIterator([]); - $this->generator = $it; - $this->generatorPosition = count($this->keyCache); - } else { - $this->generator = function () use ($iterable): Generator { - $i = 0; - /** @var Generator $it */ - $it = is_callable($iterable) ? $iterable() : $iterable; - /** @var mixed $key */ - foreach ($it as $key => $value) { - if ($this->isStringable($key)) { - yield (string) $key => $value; - } else { - yield (string) $i => $value; - } - ++$i; - } - }; - } - } - - /** - * @template Value - * - * @param callable():(\Generator) $operation - * - * @return static - * - * @psalm-mutation-free - */ - protected function withOperation($operation): Map - { - /** @psalm-suppress UnsafeInstantiation */ - return new static($operation); - } - - /** - * Returns the first pair in the map. - * - * @return Pair - */ - public function first(): Pair - { - foreach ($this as $key => $value) { - return new Pair($key, $value); - } - throw new OutOfBoundsException('Cannot grab first element of an empty map'); - } - - /** - * Returns the last pair in the map. - * - * @return Pair - */ - public function last(): Pair - { - $array = $this->toArray(); - if (count($array) === 0) { - throw new OutOfBoundsException('Cannot grab last element of an empty map'); - } - - $key = array_key_last($array); - - return new Pair($key, $array[$key]); - } - - /** - * Returns the pair at the nth position of the map. - * - * @return Pair - */ - public function skip(int $position): Pair - { - $i = 0; - foreach ($this as $key => $value) { - if ($i === $position) { - return new Pair($key, $value); - } - ++$i; - } - - throw new OutOfBoundsException(sprintf('Cannot skip to a pair at position: %s', $position)); - } - - /** - * Returns the keys in the map in order. - * - * @return ArrayList - * - * @psalm-suppress UnusedForeachValue - */ - public function keys(): ArrayList - { - return ArrayList::fromIterable((function () { - foreach ($this as $key => $value) { - yield $key; - } - })()); - } - - /** - * Returns the pairs in the map in order. - * - * @return ArrayList> - */ - public function pairs(): ArrayList - { - return ArrayList::fromIterable((function () { - foreach ($this as $key => $value) { - yield new Pair($key, $value); - } - })()); - } - - /** - * Create a new map sorted by keys. Natural ordering will be used if no comparator is provided. - * - * @param (callable(string, string):int)|null $comparator - * - * @return static - */ - public function ksorted(callable $comparator = null): Map - { - return $this->withOperation(function () use ($comparator) { - $pairs = $this->pairs()->sorted(static function (Pair $x, Pair $y) use ($comparator) { - if ($comparator) { - return $comparator($x->getKey(), $y->getKey()); - } - - return $x->getKey() <=> $y->getKey(); - }); - - foreach ($pairs as $pair) { - yield $pair->getKey() => $pair->getValue(); - } - }); - } - - /** - * Returns the values in the map in order. - * - * @return ArrayList - */ - public function values(): ArrayList - { - return ArrayList::fromIterable((function () { - yield from $this; - })()); - } - - /** - * Creates a new map using exclusive or on the keys. - * - * @param iterable $map - * - * @return static - */ - public function xor(iterable $map): Map - { - return $this->withOperation(function () use ($map) { - $map = Map::fromIterable($map); - foreach ($this as $key => $value) { - if (!$map->hasKey($key)) { - yield $key => $value; - } - } - - foreach ($map as $key => $value) { - if (!$this->hasKey($key)) { - yield $key => $value; - } - } - }); - } - - /** - * @template NewValue - * - * @param iterable $values - * - * @return static - * - * @psalm-mutation-free - */ - public function merge(iterable $values): Map - { - return $this->withOperation(function () use ($values) { - $tbr = $this->toArray(); - $values = Map::fromIterable($values); - - foreach ($values as $key => $value) { - $tbr[$key] = $value; - } - - yield from $tbr; - }); - } - - /** - * Creates a union of this and the provided map. The items in the original map take precedence. - * - * @param iterable $map - * - * @return static - */ - public function union(iterable $map): Map - { - return $this->withOperation(function () use ($map) { - $map = Map::fromIterable($map)->toArray(); - $x = $this->toArray(); - - yield from $x; - - foreach ($map as $key => $value) { - if (!array_key_exists($key, $x)) { - yield $key => $value; - } - } - }); - } - - /** - * Creates a new map from the existing one filtering the values based on the keys that don't exist in the provided map. - * - * @param iterable $map - * - * @return static - */ - public function intersect(iterable $map): Map - { - return $this->withOperation(function () use ($map) { - $map = Map::fromIterable($map)->toArray(); - foreach ($this as $key => $value) { - if (array_key_exists($key, $map)) { - yield $key => $value; - } - } - }); - } - - /** - * Creates a new map from the existing one filtering the values based on the keys that also exist in the provided map. - * - * @param iterable $map - * - * @return static - */ - public function diff(iterable $map): Map - { - return $this->withOperation(function () use ($map) { - $map = Map::fromIterable($map)->toArray(); - foreach ($this as $key => $value) { - if (!array_key_exists($key, $map)) { - yield $key => $value; - } - } - }); - } - - /** - * Gets the value with the provided key. If a default value is provided, it will return the default instead of throwing an error when the key does not exist. - * - * @template TDefault - * - * @param TDefault $default - * - * @throws OutOfBoundsException - * - * @return (func_num_args() is 1 ? TValue : TValue|TDefault) - */ - public function get(string $key, $default = null) - { - if (!$this->offsetExists($key)) { - if (func_num_args() === 1) { - throw new OutOfBoundsException(sprintf('Cannot get item in sequence with key: %s', $key)); - } - - return $default; - } - - return $this->offsetGet($key); - } - - public function jsonSerialize(): mixed - { - if ($this->isEmpty()) { - return new stdClass(); - } - - return parent::jsonSerialize(); - } - - public function getAsString(string $key, mixed $default = null): string - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - $tbr = TypeCaster::toString($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'string'); - } - - return $tbr; - } - - public function getAsInt(string $key, mixed $default = null): int - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - $tbr = TypeCaster::toInt($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'int'); - } - - return $tbr; - } - - public function getAsFloat(string $key, mixed $default = null): float - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - $tbr = TypeCaster::toFloat($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'float'); - } - - return $tbr; - } - - public function getAsBool(string $key, mixed $default = null): bool - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - $tbr = TypeCaster::toBool($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'bool'); - } - - return $tbr; - } - - /** - * @return null - */ - public function getAsNull(string $key, mixed $default = null) - { - if (func_num_args() === 1) { - /** @psalm-suppress UnusedMethodCall */ - $this->get($key); - } - - return TypeCaster::toNull(); - } - - /** - * @template U - * - * @param class-string $class - * - * @return U - */ - public function getAsObject(string $key, string $class, mixed $default = null): object - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - $tbr = TypeCaster::toClass($value, $class); - if ($tbr === null) { - throw new RuntimeTypeException($value, $class); - } - - return $tbr; - } - - /** - * @return Map - */ - public function getAsMap(string $key, mixed $default = null): Map - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - - if (!is_iterable($value)) { - throw new RuntimeTypeException($value, self::class); - } - - return new Map($value); - } - - /** - * @return ArrayList - */ - public function getAsArrayList(string $key, mixed $default = null): ArrayList - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - if (!is_iterable($value)) { - throw new RuntimeTypeException($value, ArrayList::class); - } - - return new ArrayList($value); - } - - /** - * @template Value - * - * @param iterable $iterable - * - * @return Map - */ - public static function fromIterable(iterable $iterable): Map - { - return new self($iterable); - } -} diff --git a/src/Types/Node.php b/src/Types/Node.php deleted file mode 100644 index b747781b..00000000 --- a/src/Types/Node.php +++ /dev/null @@ -1,102 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Laudis\Neo4j\Exception\PropertyDoesNotExistException; - -use function sprintf; - -/** - * A Node class representing a Node in cypher. - * - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter - * - * @psalm-immutable - * @psalm-immutable - * - * @extends AbstractPropertyObject> - * @extends AbstractPropertyObject|CypherMap> - */ -final class Node extends AbstractPropertyObject -{ - /** - * @param CypherList $labels - * @param CypherMap $properties - */ - public function __construct( - private readonly int $id, - private readonly CypherList $labels, - private readonly CypherMap $properties, - private readonly ?string $elementId - ) {} - - /** - * The labels on the node. - * - * @return CypherList - */ - public function getLabels(): CypherList - { - return $this->labels; - } - - /** - * The id of the node. - */ - public function getId(): int - { - return $this->id; - } - - /** - * Gets the property of the node by key. - * - * @return OGMTypes - */ - public function getProperty(string $key) - { - /** @psalm-suppress ImpureMethodCall */ - if (!$this->properties->hasKey($key)) { - throw new PropertyDoesNotExistException(sprintf('Property "%s" does not exist on node', $key)); - } - - /** @psalm-suppress ImpureMethodCall */ - return $this->properties->get($key); - } - - /** - * @psalm-suppress ImplementedReturnTypeMismatch False positive. - * - * @return array{id: int, labels: CypherList, properties: CypherMap} - */ - public function toArray(): array - { - return [ - 'id' => $this->id, - 'labels' => $this->labels, - 'properties' => $this->properties, - ]; - } - - public function getProperties(): CypherMap - { - /** @psalm-suppress InvalidReturnStatement false positive with type alias. */ - return $this->properties; - } - - public function getElementId(): ?string - { - return $this->elementId; - } -} diff --git a/src/Types/Path.php b/src/Types/Path.php deleted file mode 100644 index 2e18a9c2..00000000 --- a/src/Types/Path.php +++ /dev/null @@ -1,82 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -/** - * A Path class representing a Path in cypher. - * - * @psalm-immutable - * - * @extends AbstractPropertyObject|CypherList|CypherList, CypherList|CypherList|CypherList> - */ -final class Path extends AbstractPropertyObject -{ - /** - * @param CypherList $nodes - * @param CypherList $relationships - * @param CypherList $ids - */ - public function __construct( - private readonly CypherList $nodes, - private readonly CypherList $relationships, - private readonly CypherList $ids - ) {} - - /** - * Returns the node in the path. - * - * @return CypherList - */ - public function getNodes(): CypherList - { - return $this->nodes; - } - - /** - * Returns the relationships in the path. - * - * @return CypherList - */ - public function getRelationships(): CypherList - { - return $this->relationships; - } - - /** - * Returns the ids of the items in the path. - * - * @return CypherList - */ - public function getIds(): CypherList - { - return $this->ids; - } - - /** - * @return array{ids: CypherList, nodes: CypherList, relationships: CypherList} - */ - public function toArray(): array - { - return [ - 'ids' => $this->ids, - 'nodes' => $this->nodes, - 'relationships' => $this->relationships, - ]; - } - - public function getProperties(): CypherMap - { - return new CypherMap($this); - } -} diff --git a/src/Types/Relationship.php b/src/Types/Relationship.php deleted file mode 100644 index 280a6679..00000000 --- a/src/Types/Relationship.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -/** - * A Relationship class representing a Relationship in cypher. - * - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter - * - * @psalm-immutable - */ -final class Relationship extends UnboundRelationship -{ - /** - * @param CypherMap $properties - */ - public function __construct( - int $id, - private readonly int $startNodeId, - private readonly int $endNodeId, - string $type, - CypherMap $properties, - ?string $elementId - ) { - parent::__construct($id, $type, $properties, $elementId); - } - - /** - * Returns the id of the start node. - */ - public function getStartNodeId(): int - { - return $this->startNodeId; - } - - /** - * Returns the id of the end node. - */ - public function getEndNodeId(): int - { - return $this->endNodeId; - } - - /** - * @psalm-suppress ImplementedReturnTypeMismatch False positive. - * - * @return array{ - * id: int, - * type: string, - * startNodeId: int, - * endNodeId: int, - * properties: CypherMap - * } - */ - public function toArray(): array - { - $tbr = parent::toArray(); - - $tbr['startNodeId'] = $this->getStartNodeId(); - $tbr['endNodeId'] = $this->getEndNodeId(); - - return $tbr; - } -} diff --git a/src/Types/Time.php b/src/Types/Time.php deleted file mode 100644 index 626c16e7..00000000 --- a/src/Types/Time.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Bolt\protocol\IStructure; -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; - -/** - * A time object represented in seconds since the unix epoch. - * - * @psalm-immutable - * - * @extends AbstractPropertyObject - */ -final class Time extends AbstractPropertyObject implements BoltConvertibleInterface -{ - public function __construct( - private readonly int $nanoSeconds, - private readonly int $tzOffsetSeconds - ) {} - - /** - * @return array{nanoSeconds: int, tzOffsetSeconds: int} - */ - public function toArray(): array - { - return ['nanoSeconds' => $this->nanoSeconds, 'tzOffsetSeconds' => $this->tzOffsetSeconds]; - } - - public function getTzOffsetSeconds(): int - { - return $this->tzOffsetSeconds; - } - - public function getNanoSeconds(): int - { - return $this->nanoSeconds; - } - - public function getProperties(): CypherMap - { - return new CypherMap($this); - } - - public function convertToBolt(): IStructure - { - return new \Bolt\protocol\v1\structures\Time($this->getNanoSeconds(), $this->getTzOffsetSeconds()); - } -} diff --git a/src/Types/UnboundRelationship.php b/src/Types/UnboundRelationship.php deleted file mode 100644 index 6f7a53a4..00000000 --- a/src/Types/UnboundRelationship.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Laudis\Neo4j\Exception\PropertyDoesNotExistException; - -use function sprintf; - -/** - * A relationship without any nodes attached to it. - * - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter - * - * @psalm-immutable - * - * @extends AbstractPropertyObject> - */ -class UnboundRelationship extends AbstractPropertyObject -{ - /** - * @param CypherMap $properties - */ - public function __construct( - private readonly int $id, - private readonly string $type, - private readonly CypherMap $properties, - private readonly ?string $elementId - ) {} - - public function getElementId(): ?string - { - return $this->elementId; - } - - public function getId(): int - { - return $this->id; - } - - public function getType(): string - { - return $this->type; - } - - public function getProperties(): CypherMap - { - /** @psalm-suppress InvalidReturnStatement false positive with type alias. */ - return $this->properties; - } - - /** - * @psalm-suppress ImplementedReturnTypeMismatch False positive. - * - * @return array{id: int, type: string, properties: CypherMap} - */ - public function toArray(): array - { - return [ - 'id' => $this->getId(), - 'type' => $this->getType(), - 'properties' => $this->getProperties(), - ]; - } - - /** - * Gets the property of the relationship by key. - * - * @return OGMTypes - */ - public function getProperty(string $key) - { - /** @psalm-suppress ImpureMethodCall */ - if (!$this->properties->hasKey($key)) { - throw new PropertyDoesNotExistException(sprintf('Property "%s" does not exist on relationship', $key)); - } - - /** @psalm-suppress ImpureMethodCall */ - return $this->properties->get($key); - } -} diff --git a/src/Types/WGS843DPoint.php b/src/Types/WGS843DPoint.php deleted file mode 100644 index 7000491c..00000000 --- a/src/Types/WGS843DPoint.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; -use Laudis\Neo4j\Contracts\PointInterface; - -/** - * A WGS84 Point in three-dimensional space. - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-wgs84-3d - * - * @psalm-immutable - * - * @psalm-import-type Crs from PointInterface - */ -final class WGS843DPoint extends Abstract3DPoint implements PointInterface, BoltConvertibleInterface -{ - public const SRID = 4979; - public const CRS = 'wgs-84-3d'; - - public function getSrid(): int - { - return self::SRID; - } - - public function getLongitude(): float - { - return $this->getX(); - } - - public function getLatitude(): float - { - return $this->getY(); - } - - public function getHeight(): float - { - return $this->getZ(); - } - - public function getCrs(): string - { - return self::CRS; - } -} diff --git a/src/Types/WGS84Point.php b/src/Types/WGS84Point.php deleted file mode 100644 index da0c4f37..00000000 --- a/src/Types/WGS84Point.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use Laudis\Neo4j\Contracts\BoltConvertibleInterface; -use Laudis\Neo4j\Contracts\PointInterface; - -/** - * A WGS84 Point in two dimensional space. - * - * @psalm-immutable - * - * @see https://neo4j.com/docs/cypher-manual/current/functions/spatial/#functions-point-wgs84-2d - * - * @psalm-import-type Crs from \Laudis\Neo4j\Contracts\PointInterface - */ -final class WGS84Point extends AbstractPoint implements PointInterface, BoltConvertibleInterface -{ - public const SRID = 4326; - public const CRS = 'wgs-84'; - - public function getSrid(): int - { - return self::SRID; - } - - public function getCrs(): string - { - return self::CRS; - } - - /** - * A numeric expression that represents the longitude/x value in decimal degrees. - */ - public function getLongitude(): float - { - return $this->getX(); - } - - /** - * A numeric expression that represents the latitude/y value in decimal degrees. - */ - public function getLatitude(): float - { - return $this->getY(); - } -} diff --git a/tests/Integration/BoltDriverIntegrationTest.php b/tests/Integration/BoltDriverIntegrationTest.php index fdceec5d..f08e50d3 100644 --- a/tests/Integration/BoltDriverIntegrationTest.php +++ b/tests/Integration/BoltDriverIntegrationTest.php @@ -24,6 +24,8 @@ final class BoltDriverIntegrationTest extends EnvironmentAwareIntegrationTest { /** * @throws Exception + * + * @psalm-suppress MixedMethodCall */ public function testValidHostname(): void { @@ -36,6 +38,8 @@ public function testValidHostname(): void /** * @throws Exception + * + * @psalm-suppress MixedMethodCall */ public function testValidUrl(): void { diff --git a/tests/Integration/ClientIntegrationTest.php b/tests/Integration/ClientIntegrationTest.php index 08e69107..60b0707f 100644 --- a/tests/Integration/ClientIntegrationTest.php +++ b/tests/Integration/ClientIntegrationTest.php @@ -41,12 +41,12 @@ public function testDifferentAuth(): void public function testAvailabilityFullImplementation(): void { - $results = $this->getSession() - ->beginTransaction() + $transaction = $this->getSession()->beginTransaction(); + $results = $transaction ->run('UNWIND [1] AS x RETURN x') ->first() ->get('x'); - + $transaction->rollback(); self::assertEquals(1, $results); } @@ -87,13 +87,8 @@ public function testValidRun(): void public function testInvalidRun(): void { - $exception = false; - try { - $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('MERGE (x:Tes0342hdm21.())', ['test' => 'a', 'otherTest' => 'b'])); - } catch (Neo4jException) { - $exception = true; - } - self::assertTrue($exception); + $this->expectException(Neo4jException::class); + $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('MERGE (x:Tes0342hdm21.())', ['test' => 'a', 'otherTest' => 'b'])); } public function testInvalidRunRetry(): void @@ -106,7 +101,8 @@ public function testInvalidRunRetry(): void } self::assertTrue($exception); - $this->getSession()->run('RETURN 1 AS one'); + $response = $this->getSession()->run('RETURN 1 AS one'); + $this->assertEquals(1, $response->first()->get('one')); } public function testValidStatement(): void @@ -131,14 +127,9 @@ public function testValidStatement(): void public function testInvalidStatement(): void { - $exception = false; - try { - $statement = Statement::create('MERGE (x:Tes0342hdm21.())', ['test' => 'a', 'otherTest' => 'b']); - $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->runStatement($statement)); - } catch (Neo4jException) { - $exception = true; - } - self::assertTrue($exception); + $this->expectException(Neo4jException::class); + $statement = Statement::create('MERGE (x:Tes0342hdm21.())', ['test' => 'a', 'otherTest' => 'b']); + $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->runStatement($statement)); } public function testStatements(): void @@ -147,7 +138,7 @@ public function testStatements(): void $response = $this->getSession()->runStatements([ Statement::create('MERGE (x:TestNode {test: $test})', $params), Statement::create('MERGE (x:OtherTestNode {test: $otherTest})', $params), - Statement::create('RETURN 1 AS x', []), + Statement::create('RETURN 1 AS x'), ]); self::assertEquals(3, $response->count()); diff --git a/tests/Integration/ComplexQueryTest.php b/tests/Integration/ComplexQueryTest.php index ccbbe718..df2b657d 100644 --- a/tests/Integration/ComplexQueryTest.php +++ b/tests/Integration/ComplexQueryTest.php @@ -198,10 +198,14 @@ public function testPeriodicCommit(): void self::markTestSkipped('Only local environment has access to local files'); } + $this->getSession()->run('MATCH (n:File) DELETE n'); + $this->getSession()->run(<<getSession()->run('MATCH (n:File) RETURN count(n) AS count'); @@ -218,9 +222,11 @@ public function testPeriodicCommitFail(): void $tsx = $this->getSession(['neo4j', 'bolt'])->beginTransaction([]); $tsx->run(<<commit(); diff --git a/tests/Integration/OGMFormatterIntegrationTest.php b/tests/Integration/OGMFormatterIntegrationTest.php index 3fe94311..55e29992 100644 --- a/tests/Integration/OGMFormatterIntegrationTest.php +++ b/tests/Integration/OGMFormatterIntegrationTest.php @@ -365,7 +365,7 @@ public function testPath(): void public function testPath2(): void { $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<<'CYPHER' -CREATE path = ((a:Node {x:$x}) - [b:HasNode {attribute: $xy}] -> (c:Node {y:$y}) - [d:HasNode {attribute: $yz}] -> (e:Node {z:$z})) +CREATE path = (a:Node {x:$x}) - [b:HasNode {attribute: $xy}] -> (c:Node {y:$y}) - [d:HasNode {attribute: $yz}] -> (e:Node {z:$z}) RETURN path CYPHER, ['x' => 'x', 'xy' => 'xy', 'y' => 'y', 'yz' => 'yz', 'z' => 'z'])); diff --git a/tests/Integration/TransactionIntegrationTest.php b/tests/Integration/TransactionIntegrationTest.php index dea054b4..488e8133 100644 --- a/tests/Integration/TransactionIntegrationTest.php +++ b/tests/Integration/TransactionIntegrationTest.php @@ -13,13 +13,9 @@ namespace Laudis\Neo4j\Tests\Integration; -use Laudis\Neo4j\Bolt\BoltDriver; -use Laudis\Neo4j\Bolt\Connection; -use Laudis\Neo4j\Bolt\ConnectionPool; use Laudis\Neo4j\Databags\Statement; use Laudis\Neo4j\Exception\Neo4jException; -use ReflectionClass; -use Throwable; +use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; final class TransactionIntegrationTest extends EnvironmentAwareIntegrationTest { @@ -224,17 +220,8 @@ public function testCommitInvalid(): void self::assertFalse($tsx->isRolledBack()); self::assertTrue($tsx->isCommitted()); - $exception = false; - try { - $tsx->commit(); - } catch (Throwable) { - $exception = true; - } - self::assertTrue($exception); - - self::assertTrue($tsx->isFinished()); - self::assertFalse($tsx->isRolledBack()); - self::assertTrue($tsx->isCommitted()); + $this->expectException(Neo4jException::class); + $tsx->commit(); } public function testRollbackValid(): void @@ -256,17 +243,8 @@ public function testRollbackInvalid(): void self::assertTrue($tsx->isRolledBack()); self::assertFalse($tsx->isCommitted()); - $exception = false; - try { - $tsx->rollback(); - } catch (Throwable) { - $exception = true; - } - self::assertTrue($exception); - - self::assertTrue($tsx->isFinished()); - self::assertTrue($tsx->isRolledBack()); - self::assertFalse($tsx->isCommitted()); + $this->expectException(Neo4jException::class); + $tsx->rollback(); } // /** @@ -304,9 +282,7 @@ public function testRollbackInvalid(): void // self::assertCount(3, $cache[$key]); // } - /** - * @doesNotPerformAssertions - */ + #[DoesNotPerformAssertions] public function testTransactionRunNoConsumeResult(): void { $tsx = $this->getSession()->beginTransaction([]); @@ -315,9 +291,7 @@ public function testTransactionRunNoConsumeResult(): void $tsx->commit(); } - /** - * @doesNotPerformAssertions - */ + #[DoesNotPerformAssertions] public function testTransactionRunNoConsumeButSaveResult(): void { $tsx = $this->getSession()->beginTransaction([]); diff --git a/tests/Unit/Authentication/AuthenticateTest.php b/tests/Unit/Authentication/AuthenticateTest.php new file mode 100644 index 00000000..2df1b5b1 --- /dev/null +++ b/tests/Unit/Authentication/AuthenticateTest.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Tests\Unit\Authentication; + +use Laudis\Neo4j\Authentication\Authenticate; +use Laudis\Neo4j\Authentication\BasicAuth; +use Laudis\Neo4j\Authentication\KerberosAuth; +use Laudis\Neo4j\Authentication\NoAuth; +use Laudis\Neo4j\Authentication\OpenIDConnectAuth; +use Laudis\Neo4j\Common\Uri; +use PHPUnit\Framework\TestCase; +use Psr\Http\Message\UriInterface; + +class AuthenticateTest extends TestCase +{ + public function testBasic(): void + { + $auth = Authenticate::basic('username', 'password'); + + $this->assertEquals(new BasicAuth('username', 'password'), $auth); + } + + public function testKerberos(): void + { + $auth = Authenticate::kerberos('token'); + + $this->assertEquals(new KerberosAuth('token'), $auth); + } + + public function testOIDC(): void + { + $auth = Authenticate::oidc('oidc'); + + $this->assertEquals(new OpenIDConnectAuth('oidc'), $auth); + } + + public function testDisabled(): void + { + $auth = Authenticate::disabled(); + + $this->assertEquals(new NoAuth(), $auth); + } + + /** + * @dataProvider generateUriCombinations + */ + public function testFromUrlNoAuth(UriInterface $uri, BasicAuth|NoAuth $expected): void + { + $this->assertEquals($expected, Authenticate::fromUrl($uri)); + } + + public static function generateUriCombinations(): array + { + return [ + [Uri::create('https://test:teste@localhost'), new BasicAuth('test', 'teste')], + [Uri::create('bolt://test:teste@localhost'), new BasicAuth('test', 'teste')], + [Uri::create('bolt+s://test:teste@localhost'), new BasicAuth('test', 'teste')], + [Uri::create('bolt+ssc://test:teste@localhost'), new BasicAuth('test', 'teste')], + [Uri::create('wrong://test:teste@localhost'), new BasicAuth('test', 'teste')], + [Uri::create('wrong://test:@localhost'), new BasicAuth('test', '')], + [Uri::create('https://localhost'), new NoAuth()], + [Uri::create('http://localhost'), new NoAuth()], + [Uri::create('bolt://localhost'), new NoAuth()], + [Uri::create('bolt+ssc://localhost'), new NoAuth()], + [Uri::create('bolt+s://localhost'), new NoAuth()], + [Uri::create('neo4j://localhost'), new NoAuth()], + [Uri::create('neo4j+ssc://localhost'), new NoAuth()], + [Uri::create('neo4j+s://localhost'), new NoAuth()], + ]; + } +} diff --git a/tests/Unit/Authentication/BasicAuthTest.php b/tests/Unit/Authentication/BasicAuthTest.php new file mode 100644 index 00000000..0de07f3d --- /dev/null +++ b/tests/Unit/Authentication/BasicAuthTest.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Tests\Unit\Authentication; + +use Bolt\helpers\Auth; +use Bolt\protocol\Response; +use Laudis\Neo4j\Authentication\BasicAuth; +use Laudis\Neo4j\Contracts\AuthenticateInterface; +use PHPUnit\Framework\TestCase; + +class BasicAuthTest extends TestCase +{ + use TestsAuth; + + public static function provideHttp(): array + { + return [ + ['Basic test:test', 'abc', new BasicAuth('test', 'test')], + ['Basic test1:test2', 'abcd', new BasicAuth('test1', 'test2')], + ['Basic test:', 'acerq', new BasicAuth('test', '')], + ]; + } + + public static function provideBolt(): array + { + return [ + self::createBasics('abc', 'test', 'test', Response::SIGNATURE_SUCCESS), + self::createBasics('abcd', 'test1', 'test2', Response::SIGNATURE_SUCCESS), + self::createBasics('abcd', 'test1', 'test2', Response::SIGNATURE_FAILURE), + self::createBasics('abcd', 'test1', '', Response::SIGNATURE_FAILURE), + ]; + } + + /** + * @return array{0: string, 1: AuthenticateInterface, 2: array, 3: int} + */ + private static function createBasics(string $userAgent, string $user, string $pass, int $code): array + { + return [$userAgent, new BasicAuth($user, $pass), Auth::basic($user, $pass, $userAgent), $code, $response]; + } + + public static function provideToString(): array + { + return [ + ['Basic test:######', new BasicAuth('test', 'test')], + ['Basic test:######', new BasicAuth('test', 'test2')], + ['Basic test1:######', new BasicAuth('test1', 'test2')], + ['Basic test1:######', new BasicAuth('test1', '')], + ]; + } +} diff --git a/tests/Unit/Authentication/KerberosTest.php b/tests/Unit/Authentication/KerberosTest.php new file mode 100644 index 00000000..971d323c --- /dev/null +++ b/tests/Unit/Authentication/KerberosTest.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Tests\Unit\Authentication; + +use Bolt\helpers\Auth; +use Bolt\protocol\Response; +use Laudis\Neo4j\Authentication\KerberosAuth; +use Nyholm\Psr7\Request; +use PHPUnit\Framework\TestCase; +use RuntimeException; + +class KerberosTest extends TestCase +{ + use TestsAuth { + testAuthenticateHttp as disabledTestAuthenticateHttp; + } + + public function testAuthenticateHttp(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Cannot authenticate http requests with Kerberos, use bolt instead.'); + + (new KerberosAuth('abc'))->authenticateHttp(new Request('GET', ''), 'abc'); + } + + public static function provideHttp(): array + { + return []; + } + + public static function provideBolt(): array + { + return [ + ['abc', new KerberosAuth('Tabc'), Auth::kerberos('Tabc', 'abc'), Response::SIGNATURE_SUCCESS], + ['abcd', new KerberosAuth('Tabcd'), Auth::kerberos('Tabcd', 'abcd'), Response::SIGNATURE_SUCCESS], + ['abcd', new KerberosAuth('Tabcd'), Auth::kerberos('Tabcd', 'abcd'), Response::SIGNATURE_FAILURE], + ['abcde', new KerberosAuth('Tabcde'), Auth::kerberos('Tabcde', 'abcde'), Response::SIGNATURE_FAILURE], + ]; + } + + public static function provideToString(): array + { + return [ + ['Kerberos abc', new KerberosAuth('abc')], + ['Kerberos abcd', new KerberosAuth('abcd')], + ]; + } +} diff --git a/tests/Unit/Authentication/NoAuthTest.php b/tests/Unit/Authentication/NoAuthTest.php new file mode 100644 index 00000000..4ceb0167 --- /dev/null +++ b/tests/Unit/Authentication/NoAuthTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Tests\Unit\Authentication; + +use Bolt\helpers\Auth; +use Bolt\protocol\Response; +use Laudis\Neo4j\Authentication\NoAuth; +use PHPUnit\Framework\TestCase; + +class NoAuthTest extends TestCase +{ + use TestsAuth; + + public static function provideHttp(): array + { + return [ + ['', 'abc', new NoAuth()], + ['', 'abcd', new NoAuth()], + ['', 'acerq', new NoAuth()], + ]; + } + + public static function provideBolt(): array + { + return [ + ['abc', new NoAuth(), Auth::none('abc'), Response::SIGNATURE_SUCCESS], + ['abcd', new NoAuth(), Auth::none('abcd'), Response::SIGNATURE_SUCCESS], + ['abcd', new NoAuth(), Auth::none('abcd'), Response::SIGNATURE_FAILURE], + ['abcde', new NoAuth(), Auth::none('abcde'), Response::SIGNATURE_FAILURE], + ]; + } + + public static function provideToString(): array + { + return [ + ['No Auth', new NoAuth()], + ]; + } +} diff --git a/tests/Unit/Authentication/OIDCTest.php b/tests/Unit/Authentication/OIDCTest.php new file mode 100644 index 00000000..4b3be421 --- /dev/null +++ b/tests/Unit/Authentication/OIDCTest.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Tests\Unit\Authentication; + +use Bolt\helpers\Auth; +use Bolt\protocol\Response; +use Laudis\Neo4j\Authentication\OpenIDConnectAuth; +use Nyholm\Psr7\Request; +use PHPUnit\Framework\TestCase; +use RuntimeException; + +class OIDCTest extends TestCase +{ + use TestsAuth { + testAuthenticateHttp as disabledTestAuthenticateHttp; + } + + public function testAuthenticateHttp(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Cannot authenticate http requests with OpenID Connect, use bolt instead.'); + + (new OpenIDConnectAuth('abc'))->authenticateHttp(new Request('GET', ''), 'abc'); + } + + public static function provideHttp(): array + { + return []; + } + + public static function provideBolt(): array + { + return [ + ['abc', new OpenIDConnectAuth('Tabc'), Auth::bearer('Tabc', 'abc'), Response::SIGNATURE_SUCCESS], + ['abcd', new OpenIDConnectAuth('Tabcd'), Auth::bearer('Tabcd', 'abcd'), Response::SIGNATURE_SUCCESS], + ['abcd', new OpenIDConnectAuth('Tabcd'), Auth::bearer('Tabcd', 'abcd'), Response::SIGNATURE_FAILURE], + ['abcde', new OpenIDConnectAuth('Tabcde'), Auth::bearer('Tabcde', 'abcde'), Response::SIGNATURE_FAILURE], + ]; + } + + public static function provideToString(): array + { + return [ + ['OpenId abc', new OpenIDConnectAuth('abc')], + ['OpenId abcd', new OpenIDConnectAuth('abcd')], + ]; + } +} diff --git a/tests/Unit/Authentication/TestsAuth.php b/tests/Unit/Authentication/TestsAuth.php new file mode 100644 index 00000000..0ab6cee0 --- /dev/null +++ b/tests/Unit/Authentication/TestsAuth.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Tests\Unit\Authentication; + +use Bolt\protocol\Response; +use Bolt\protocol\V4_4; +use Laudis\Neo4j\Contracts\AuthenticateInterface; +use Laudis\Neo4j\Databags\Neo4jError; +use Laudis\Neo4j\Exception\Neo4jException; +use Nyholm\Psr7\Request; +use PHPUnit\Framework\TestCase; + +/** + * @mixin TestCase + */ +trait TestsAuth +{ + /** + * @dataProvider provideHttp + */ + public function testAuthenticateHttp(string $authHeader, string $userAgent, AuthenticateInterface $instance): void + { + $request = new Request('GET', ''); + + $result = $instance->authenticateHttp($request, $userAgent); + + $this->assertEquals($authHeader, $result->getHeaderLine('Authorization')); + $this->assertEquals($userAgent, $result->getHeaderLine('User-Agent')); + } + + /** + * @dataProvider provideBolt + */ + public function testAuthenticateBolt( + string $userAgent, + AuthenticateInterface $instance, + array $helloMessage, + int $responseSignature + ): void { + $bolt = $this->getMockBuilder(V4_4::class) + ->disableOriginalConstructor() + ->getMock(); + + $response = $this->getMockBuilder(Response::class) + ->disableOriginalConstructor() + ->getMock(); + + $response->method('getSignature') + ->willReturn($responseSignature); + + $contents = ['server' => 'a', 'connection_id' => 'b', 'hints' => []]; + if ($responseSignature === Response::SIGNATURE_FAILURE) { + $contents = ['code' => 'a.b.c.d', 'message' => 'hello']; + } + + $response->method('getContent') + ->willReturn($contents); + + $bolt->expects($this->once()) + ->method('hello') + ->with($helloMessage) + ->willReturn($response); + + if ($responseSignature === Response::SIGNATURE_FAILURE) { + $this->expectException(Neo4jException::class); + + $exception = new Neo4jException([Neo4jError::fromMessageAndCode('a.b.c.d', 'hello')]); + $this->expectExceptionMessage($exception->getMessage()); + } + + $response = $instance->authenticate($bolt, $userAgent); + + $this->assertEquals($contents, $response); + } + + /** + * @dataProvider provideToString + */ + public function testToString(string $expected, AuthenticateInterface $instance): void + { + $this->assertEquals($expected, (string) $instance); + } + + /** + * @return list + */ + abstract public static function provideHttp(): array; + + /** + * @return list + */ + abstract public static function provideBolt(): array; + + /** + * @return list + */ + abstract public static function provideToString(): array; +} diff --git a/src/Contracts/BoltConvertibleInterface.php b/tests/Unit/Bolt/BoltConnectionTest.php similarity index 65% rename from src/Contracts/BoltConvertibleInterface.php rename to tests/Unit/Bolt/BoltConnectionTest.php index cf60a978..c54b329e 100644 --- a/src/Contracts/BoltConvertibleInterface.php +++ b/tests/Unit/Bolt/BoltConnectionTest.php @@ -11,11 +11,10 @@ * file that was distributed with this source code. */ -namespace Laudis\Neo4j\Contracts; +namespace Laudis\Neo4j\Tests\Unit\Bolt; -use Bolt\protocol\IStructure; +use PHPUnit\Framework\TestCase; -interface BoltConvertibleInterface +class BoltConnectionTest extends TestCase { - public function convertToBolt(): IStructure; } diff --git a/tests/Unit/BoltConnectionPoolTest.php b/tests/Unit/BoltConnectionPoolTest.php index 48267a5c..3f3e089d 100644 --- a/tests/Unit/BoltConnectionPoolTest.php +++ b/tests/Unit/BoltConnectionPoolTest.php @@ -13,6 +13,7 @@ namespace Laudis\Neo4j\Tests\Unit; +use Bolt\protocol\V5; use Generator; use Laudis\Neo4j\Authentication\Authenticate; use Laudis\Neo4j\Bolt\BoltConnection; @@ -147,8 +148,12 @@ private function setupPool(Generator $semaphoreGenerator): void ->willReturn($semaphoreGenerator); $this->factory = $this->createMock(BoltFactory::class); + $boltConnection = $this->createMock(BoltConnection::class); + $boltConnection->method('protocol')->willReturn($this->createMock(V5::class)); $this->factory->method('createConnection') - ->willReturn($this->createMock(BoltConnection::class)); + ->willReturn($boltConnection); + $this->factory->method('reuseConnection') + ->willReturnCallback(fn (MockObject $x): MockObject => $x); $this->pool = new ConnectionPool( $this->semaphore, $this->factory, new ConnectionRequestData( diff --git a/tests/Unit/BoltFactoryTest.php b/tests/Unit/BoltFactoryTest.php index a0db0dc7..6d91f71b 100644 --- a/tests/Unit/BoltFactoryTest.php +++ b/tests/Unit/BoltFactoryTest.php @@ -14,8 +14,6 @@ namespace Laudis\Neo4j\Tests\Unit; use Bolt\connection\IConnection; -use Bolt\packstream\v1\Packer; -use Bolt\packstream\v1\Unpacker; use Bolt\protocol\ServerState; use Bolt\protocol\V5; use Laudis\Neo4j\Authentication\Authenticate; @@ -44,7 +42,7 @@ protected function setUp(): void $protocolFactory = $this->createMock(ProtocolFactory::class); $protocolFactory->method('createProtocol') ->willReturnCallback(static fn (IConnection $connection) => [ - new V5(new Packer(), new Unpacker(), $connection, new ServerState()), + new V5(1, $connection, new ServerState()), ['server' => 'abc', 'connection_id' => 'i'], ]); diff --git a/tests/Unit/CypherListTest.php b/tests/Unit/CypherListTest.php index d10fc496..b5307df0 100644 --- a/tests/Unit/CypherListTest.php +++ b/tests/Unit/CypherListTest.php @@ -264,6 +264,11 @@ public function testIteration(): void self::assertEquals(3, $counter); } + /** + * @psalm-suppress UnevaluatedCode + * @psalm-suppress NoValue + * @psalm-suppress UnusedVariable + */ public function testIterationEmpty(): void { $counter = 0; @@ -443,8 +448,6 @@ public function testSlice(): void return $x; }); - /** @var int $sumBefore */ - /** @var int $sumAfter */ $start = $range->get(0); self::assertEquals(5, $start); diff --git a/tests/Unit/CypherMapTest.php b/tests/Unit/CypherMapTest.php index b79cb49f..848112b7 100644 --- a/tests/Unit/CypherMapTest.php +++ b/tests/Unit/CypherMapTest.php @@ -266,6 +266,11 @@ public function testIteration(): void self::assertEquals(3, $counter); } + /** + * @psalm-suppress UnevaluatedCode + * @psalm-suppress UnusedVariable + * @psalm-suppress NoValue + */ public function testIterationEmpty(): void { $counter = 0; @@ -420,6 +425,7 @@ public function testSkipInvalid(): void public function testInvalidConstruct(): void { + /** @psalm-suppress MissingTemplateParam */ $map = new CypherMap(new class() implements IteratorAggregate { public function getIterator(): Generator { diff --git a/tests/Unit/DNSAddressResolverTest.php b/tests/Unit/DNSAddressResolverTest.php index 8f4dd625..c109f0c5 100644 --- a/tests/Unit/DNSAddressResolverTest.php +++ b/tests/Unit/DNSAddressResolverTest.php @@ -28,16 +28,16 @@ protected function setUp(): void public function testResolverGhlenDotCom(): void { - $records = [...$this->resolver->getAddresses('test.ghlen.com')]; + $records = iterator_to_array($this->resolver->getAddresses('www.cloudflare.com'), false); - $this->assertEqualsCanonicalizing(['test.ghlen.com', '123.123.123.123', '123.123.123.124'], $records); + $this->assertEqualsCanonicalizing(['www.cloudflare.com', '104.16.123.96', '104.16.124.96'], $records); $this->assertNotEmpty($records); - $this->assertEquals('test.ghlen.com', $records[0]); + $this->assertEquals('www.cloudflare.com', $records[0] ?? ''); } public function testResolverGoogleDotComReverse(): void { - $records = [...$this->resolver->getAddresses('8.8.8.8')]; + $records = iterator_to_array($this->resolver->getAddresses('8.8.8.8'), false); $this->assertNotEmpty($records); $this->assertContains('8.8.8.8', $records); @@ -45,6 +45,7 @@ public function testResolverGoogleDotComReverse(): void public function testBogus(): void { - $this->assertEquals(['bogus'], [...$this->resolver->getAddresses('bogus')]); + $addresses = iterator_to_array($this->resolver->getAddresses('bogus'), false); + $this->assertEquals(['bogus'], $addresses); } } diff --git a/tests/Unit/ParameterHelperTest.php b/tests/Unit/ParameterHelperTest.php index 28c1a25d..83ce18a0 100644 --- a/tests/Unit/ParameterHelperTest.php +++ b/tests/Unit/ParameterHelperTest.php @@ -32,7 +32,10 @@ final class ParameterHelperTest extends TestCase public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - /** @psalm-suppress MixedPropertyTypeCoercion */ + /** + * @psalm-suppress MixedPropertyTypeCoercion + * @psalm-suppress MissingTemplateParam + */ self::$invalidIterable = new class() implements Iterator { private bool $initial = true; @@ -162,7 +165,7 @@ public function testDateTime(): void $date = ParameterHelper::asParameter(new DateTime('now', new DateTimeZone('Europe/Brussels')), ConnectionProtocol::BOLT_V44()); self::assertInstanceOf(DateTimeZoneId::class, $date); - self::assertEquals('Europe/Brussels', $date->tz_id()); + self::assertEquals('Europe/Brussels', $date->tz_id); } public function testDateTime5(): void