From c97dcd0cc1d4fdad127c06cc1987cba8f6e291db Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Wed, 25 Oct 2023 12:11:06 +0530 Subject: [PATCH 01/59] updated bolt --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7fcc5f98..0d1a0b1b 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "psr/http-factory": "^1.0", "psr/http-client": "^1.0", "php-http/message": "^1.0", - "stefanak-michal/bolt": "^5.1", + "stefanak-michal/bolt": "^6.1", "symfony/polyfill-php80": "^1.2", "psr/simple-cache": ">=2.0", "ext-json": "*" From 73df2479804e47306ffea72898637b4d499483e2 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Wed, 25 Oct 2023 12:12:33 +0530 Subject: [PATCH 02/59] added default baseline suggestions --- psalm.xml | 2 ++ tests/Unit/BoltFactoryTest.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/psalm.xml b/psalm.xml index 9368a22f..6765f72a 100755 --- a/psalm.xml +++ b/psalm.xml @@ -9,6 +9,8 @@ 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/tests/Unit/BoltFactoryTest.php b/tests/Unit/BoltFactoryTest.php index a0db0dc7..79deb963 100644 --- a/tests/Unit/BoltFactoryTest.php +++ b/tests/Unit/BoltFactoryTest.php @@ -44,7 +44,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'], ]); From 176d747ec8d73c785d27681bd03a9e442d1d8ce9 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Wed, 25 Oct 2023 13:55:27 +0530 Subject: [PATCH 03/59] completely tested and finished authentication layer --- psalm-baseline.xml | 181 ------------------ src/Authentication/Authenticate.php | 6 +- src/Authentication/BasicAuth.php | 23 +-- src/Authentication/KerberosAuth.php | 27 +-- src/Authentication/NoAuth.php | 19 +- src/Authentication/OpenIDConnectAuth.php | 28 +-- src/Contracts/AuthenticateInterface.php | 10 +- .../Unit/Authentication/AuthenticateTest.php | 82 ++++++++ tests/Unit/Authentication/BasicAuthTest.php | 62 ++++++ tests/Unit/Authentication/KerberosTest.php | 59 ++++++ tests/Unit/Authentication/NoAuthTest.php | 50 +++++ tests/Unit/Authentication/OIDCTest.php | 59 ++++++ tests/Unit/Authentication/TestsAuth.php | 109 +++++++++++ tests/Unit/BoltFactoryTest.php | 2 - 14 files changed, 483 insertions(+), 234 deletions(-) delete mode 100644 psalm-baseline.xml create mode 100644 tests/Unit/Authentication/AuthenticateTest.php create mode 100644 tests/Unit/Authentication/BasicAuthTest.php create mode 100644 tests/Unit/Authentication/KerberosTest.php create mode 100644 tests/Unit/Authentication/NoAuthTest.php create mode 100644 tests/Unit/Authentication/OIDCTest.php create mode 100644 tests/Unit/Authentication/TestsAuth.php 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/src/Authentication/Authenticate.php b/src/Authentication/Authenticate.php index d0504cac..97989c8a 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; @@ -24,6 +23,8 @@ * Factory responsible for creating authentication logic. * * @psalm-immutable + * + * @api */ final class Authenticate { @@ -72,7 +73,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,6 +83,7 @@ public static function fromUrl(UriInterface $uri): AuthenticateInterface $userInfo = $uri->getUserInfo(); if (substr_count($userInfo, ':') === 1) { + /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ [$user, $pass] = explode(':', $userInfo); return self::basic($user, $pass); diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index 85a34f57..d1982983 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -13,22 +13,25 @@ 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 Exception; 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 @@ -41,23 +44,21 @@ public function __construct( /** * @psalm-mutation-free */ - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface + public function authenticateHttp(RequestInterface $request, string $userAgent): RequestInterface { - $combo = base64_encode($this->username.':'.$this->password); - /** * @psalm-suppress ImpureMethodCall Request is a pure object: * * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message-meta.md#why-value-objects */ - return $request->withHeader('Authorization', 'Basic '.$combo) + return $request->withHeader('Authorization', sprintf('Basic %s:%s', $this->username, $this->password)) ->withHeader('User-Agent', $userAgent); } /** * @throws Exception */ - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array + public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array { $response = $bolt->hello(Auth::basic($this->username, $this->password, $userAgent)); if ($response->getSignature() === Response::SIGNATURE_FAILURE) { @@ -68,8 +69,8 @@ public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array 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 6d90dd28..146b22aa 100644 --- a/src/Authentication/KerberosAuth.php +++ b/src/Authentication/KerberosAuth.php @@ -17,17 +17,24 @@ 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 Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Exception\Neo4jException; use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; +use RuntimeException; 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 @@ -39,18 +46,12 @@ public function __construct( /** * @psalm-mutation-free */ - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface + public function authenticateHttp(RequestInterface $request, 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('Authorization', 'Kerberos '.$this->token) - ->withHeader('User-Agent', $userAgent); + throw new RuntimeException('Cannot authenticate http requests with Kerberos, use bolt instead.'); } - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array + public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array { $response = $bolt->hello(Auth::kerberos($this->token, $userAgent)); if ($response->getSignature() === Response::SIGNATURE_FAILURE) { @@ -61,8 +62,8 @@ public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array return $response->getContent(); } - 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..8239d7c2 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -17,22 +17,25 @@ 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 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 + public function authenticateHttp(RequestInterface $request, string $userAgent): RequestInterface { /** * @psalm-suppress ImpureMethodCall Request is a pure object: @@ -42,7 +45,7 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s return $request->withHeader('User-Agent', $userAgent); } - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array + public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array { $response = $bolt->hello(Auth::none($userAgent)); if ($response->getSignature() === Response::SIGNATURE_FAILURE) { @@ -53,8 +56,8 @@ public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array 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 0dbb6286..66e10fb8 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -17,14 +17,22 @@ 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 Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Exception\Neo4jException; use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; +use RuntimeException; use function sprintf; -final class OpenIDConnectAuth implements AuthenticateInterface +use Stringable; + +/** + * @internal + */ +final class OpenIDConnectAuth implements AuthenticateInterface, Stringable { /** * @psalm-external-mutation-free @@ -36,18 +44,12 @@ public function __construct( /** * @psalm-mutation-free */ - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface + public function authenticateHttp(RequestInterface $request, 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('Authorization', 'Bearer '.$this->token) - ->withHeader('User-Agent', $userAgent); + throw new RuntimeException('Cannot authenticate http requests with OpenID Connect, use bolt instead.'); } - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array + public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array { $response = $bolt->hello(Auth::bearer($this->token, $userAgent)); if ($response->getSignature() === Response::SIGNATURE_FAILURE) { @@ -58,8 +60,8 @@ public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array 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/Contracts/AuthenticateInterface.php b/src/Contracts/AuthenticateInterface.php index 428dbd4b..4a1ec4b2 100644 --- a/src/Contracts/AuthenticateInterface.php +++ b/src/Contracts/AuthenticateInterface.php @@ -15,8 +15,10 @@ 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 Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; interface AuthenticateInterface { @@ -25,17 +27,17 @@ interface AuthenticateInterface * * Authenticates a RequestInterface with the provided configuration Uri and userAgent. */ - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface; + public function authenticateHttp(RequestInterface $request, 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; + public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array; /** * Returns a string representation of the authentication. */ - public function toString(UriInterface $uri): string; + public function __toString(): string; } 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..072dc72f --- /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->authenticateBolt($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/tests/Unit/BoltFactoryTest.php b/tests/Unit/BoltFactoryTest.php index 79deb963..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; From 3dbbd7c79e4b2dd33003d16db81eeb3b7297d409 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Wed, 25 Oct 2023 16:57:04 +0530 Subject: [PATCH 04/59] removed basic objects --- src/Basic/Client.php | 104 ----------------------------- src/Basic/Driver.php | 60 ----------------- src/Basic/Session.php | 88 ------------------------ src/Basic/UnmanagedTransaction.php | 91 ------------------------- 4 files changed, 343 deletions(-) delete mode 100644 src/Basic/Client.php delete mode 100644 src/Basic/Driver.php delete mode 100644 src/Basic/Session.php delete mode 100644 src/Basic/UnmanagedTransaction.php diff --git a/src/Basic/Client.php b/src/Basic/Client.php deleted file mode 100644 index 4a9dd018..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 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 43deea77..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 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 5d14bc45..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 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 e7e235ee..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 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(); - } -} From 18a6156588f0940467922b3f12b71c7f57d024db Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sat, 4 Nov 2023 17:14:52 +0530 Subject: [PATCH 05/59] temp --- psalm.xml | 1 - src/Bolt/BoltConnection.php | 44 +++++++-------- src/Bolt/BoltDriver.php | 28 ++-------- src/Bolt/BoltUnmanagedTransaction.php | 7 +-- src/Bolt/Session.php | 54 +++++++------------ src/Bolt/SocketConnectionFactory.php | 1 + src/Bolt/StreamConnectionFactory.php | 1 + src/Bolt/SystemWideConnectionFactory.php | 4 +- src/BoltFactory.php | 5 +- src/{Bolt => Common}/UriConfiguration.php | 2 +- .../BasicConnectionFactoryInterface.php | 2 +- src/Contracts/DriverInterface.php | 4 +- src/Contracts/SessionInterface.php | 39 +++++--------- src/Contracts/TransactionInterface.php | 17 +++--- .../UnmanagedTransactionInterface.php | 10 ++-- src/Neo4j/Neo4jConnectionPool.php | 3 +- 16 files changed, 80 insertions(+), 142 deletions(-) rename src/{Bolt => Common}/UriConfiguration.php (97%) diff --git a/psalm.xml b/psalm.xml index 6765f72a..82f7d198 100755 --- a/psalm.xml +++ b/psalm.xml @@ -8,7 +8,6 @@ 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/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 9806d00d..0f3644ba 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -17,6 +17,9 @@ 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 Laudis\Neo4j\Common\ConnectionConfiguration; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Contracts\ConnectionInterface; @@ -31,9 +34,11 @@ use WeakReference; /** - * @implements ConnectionInterface + * @implements ConnectionInterface * * @psalm-import-type BoltMeta from FormatterInterface + * + * @internal */ class BoltConnection implements ConnectionInterface { @@ -45,16 +50,11 @@ 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> */ private array $subscribedResults = []; - /** - * @return array{0: V4_4|V5, 1: Connection} - */ public function getImplementation(): array { return [$this->boltProtocol, $this->connection]; @@ -64,7 +64,7 @@ public function getImplementation(): array * @psalm-mutation-free */ public function __construct( - private V4_4|V5 $boltProtocol, + private V4_4|V5|V5_1|V5_2|V5_3 $boltProtocol, private Connection $connection, private AuthenticateInterface $auth, private string $userAgent, @@ -135,7 +135,7 @@ public function getAuthentication(): AuthenticateInterface */ public function isOpen(): bool { - return in_array($this->protocol()->serverState->get(), ['DISCONNECTED', 'DEFUNCT'], true); + return in_array($this->boltProtocol->serverState->get(), ['DISCONNECTED', 'DEFUNCT'], true); } public function setTimeout(float $timeout): void @@ -163,7 +163,7 @@ public function consumeResults(): void */ public function reset(): void { - $response = $this->protocol()->reset() + $response = $this->boltProtocol->reset() ->getResponse(); $this->assertNoFailure($response); @@ -181,7 +181,7 @@ public function begin(?string $database, ?float $timeout, BookmarkHolder $holder $this->consumeResults(); $extra = $this->buildRunExtra($database, $timeout, $holder); - $response = $this->protocol() + $response = $this->boltProtocol ->begin($extra) ->getResponse(); @@ -196,7 +196,7 @@ public function begin(?string $database, ?float $timeout, BookmarkHolder $holder public function discard(?int $qid): void { $extra = $this->buildResultExtra(null, $qid); - $bolt = $this->protocol(); + $bolt = $this->boltProtocol; $response = $bolt->discard($extra) ->getResponse(); @@ -214,7 +214,7 @@ public function discard(?int $qid): void public function run(string $text, array $parameters, ?string $database, ?float $timeout, BookmarkHolder $holder): array { $extra = $this->buildRunExtra($database, $timeout, $holder); - $response = $this->protocol()->run($text, $parameters, $extra) + $response = $this->boltProtocol->run($text, $parameters, $extra) ->getResponse(); $this->assertNoFailure($response); @@ -231,7 +231,7 @@ public function run(string $text, array $parameters, ?string $database, ?float $ public function commit(): void { $this->consumeResults(); - $response = $this->protocol() + $response = $this->boltProtocol ->commit() ->getResponse(); @@ -246,18 +246,13 @@ public function commit(): void public function rollback(): void { $this->consumeResults(); - $response = $this->protocol() + $response = $this->boltProtocol ->rollback() ->getResponse(); $this->assertNoFailure($response); } - public function protocol(): V4_4|V5 - { - return $this->boltProtocol; - } - /** * Pulls a result set. * @@ -270,7 +265,8 @@ public function pull(?int $qid, ?int $fetchSize): array $extra = $this->buildResultExtra($fetchSize, $qid); $tbr = []; - foreach ($this->protocol()->pull($extra)->getResponses() as $response) { + /** @var Response $response */ + foreach ($this->boltProtocol->pull($extra)->getResponses() as $response) { $this->assertNoFailure($response); $tbr[] = $response->getContent(); @@ -282,12 +278,12 @@ public function pull(?int $qid, ?int $fetchSize): array public function __destruct() { - if (!$this->protocol()->serverState->is(ServerState::FAILED) && $this->isOpen()) { - if ($this->protocol()->serverState->is(ServerState::STREAMING, ServerState::TX_STREAMING)) { + if (!$this->boltProtocol->serverState->is(ServerState::FAILED) && $this->isOpen()) { + if ($this->boltProtocol->serverState->is(ServerState::STREAMING, ServerState::TX_STREAMING)) { $this->consumeResults(); } - $this->protocol()->goodbye(); + $this->boltProtocol->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; } @@ -326,7 +322,7 @@ private function buildResultExtra(?int $fetchSize, ?int $qid): array public function getServerState(): string { - return $this->protocol()->serverState->get(); + return $this->boltProtocol->serverState->get(); } public function subscribeResult(CypherList $result): void diff --git a/src/Bolt/BoltDriver.php b/src/Bolt/BoltDriver.php index e9830e54..2d4a4682 100644 --- a/src/Bolt/BoltDriver.php +++ b/src/Bolt/BoltDriver.php @@ -22,50 +22,31 @@ 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 UriInterface $parsedUrl, private ConnectionPool $pool, - private FormatterInterface $formatter + private 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 +56,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/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index 814c23d5..bee0a901 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -19,6 +19,7 @@ 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\ParameterHelper; @@ -65,11 +66,7 @@ public function commit(iterable $statements = []): CypherList { // 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(); - } - }); + $tbr = $this->runStatements($statements)->each(static fn (SummarizedResult $result) => $result->preload()); $this->connection->commit(); $this->isCommitted = true; diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php index a0ab8f7d..35aa3d3b 100644 --- a/src/Bolt/Session.php +++ b/src/Bolt/Session.php @@ -16,27 +16,23 @@ 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; +use Throwable; /** * A session using bolt connections. - * - * @template ResultFormat - * - * @implements SessionInterface */ final class Session implements SessionInterface { @@ -44,24 +40,23 @@ final class Session implements SessionInterface private BookmarkHolder $bookmarkHolder; /** - * @param ConnectionPool|Neo4jConnectionPool $pool - * @param FormatterInterface $formatter - * * @psalm-mutation-free */ public function __construct( /** @psalm-readonly */ private SessionConfiguration $config, - private ConnectionPoolInterface $pool, - /** - * @psalm-readonly - */ - private FormatterInterface $formatter + /** @psalm-readonly */ + private ConnectionPool|Neo4jConnectionPool $pool, + /** @psalm-readonly */ + private 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 +65,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 +88,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 +98,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 +113,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); diff --git a/src/Bolt/SocketConnectionFactory.php b/src/Bolt/SocketConnectionFactory.php index 72d071e1..6d434f81 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/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..a3ebfe1b 100644 --- a/src/Bolt/SystemWideConnectionFactory.php +++ b/src/Bolt/SystemWideConnectionFactory.php @@ -13,9 +13,9 @@ namespace Laudis\Neo4j\Bolt; -use function extension_loaded; - +use Laudis\Neo4j\Common\UriConfiguration; use Laudis\Neo4j\Contracts\BasicConnectionFactoryInterface; +use function extension_loaded; /** * Singleton connection factory based on the installed extensions. diff --git a/src/BoltFactory.php b/src/BoltFactory.php index 3ad0fcc8..495f904e 100644 --- a/src/BoltFactory.php +++ b/src/BoltFactory.php @@ -13,14 +13,12 @@ namespace Laudis\Neo4j; -use function explode; - use Laudis\Neo4j\Bolt\BoltConnection; 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; @@ -28,6 +26,7 @@ use Laudis\Neo4j\Databags\SessionConfiguration; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Enum\ConnectionProtocol; +use function explode; /** * Small wrapper around the bolt library to easily guarantee only bolt version 3 and up will be created and authenticated. diff --git a/src/Bolt/UriConfiguration.php b/src/Common/UriConfiguration.php similarity index 97% rename from src/Bolt/UriConfiguration.php rename to src/Common/UriConfiguration.php index c5e12571..e12b760a 100644 --- a/src/Bolt/UriConfiguration.php +++ b/src/Common/UriConfiguration.php @@ -11,7 +11,7 @@ * file that was distributed with this source code. */ -namespace Laudis\Neo4j\Bolt; +namespace Laudis\Neo4j\Common; final class UriConfiguration { 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/DriverInterface.php b/src/Contracts/DriverInterface.php index ea0ca2b3..6ca30d17 100644 --- a/src/Contracts/DriverInterface.php +++ b/src/Contracts/DriverInterface.php @@ -20,15 +20,13 @@ /** * 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 + * @return SessionInterface * * @psalm-mutation-free */ diff --git a/src/Contracts/SessionInterface.php b/src/Contracts/SessionInterface.php index 57eeeb5b..47f258bc 100644 --- a/src/Contracts/SessionInterface.php +++ b/src/Contracts/SessionInterface.php @@ -15,75 +15,62 @@ use Laudis\Neo4j\Databags\Bookmark; use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Exception\Neo4jException; -use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\ArrayList; /** * A lightweight container for causally chained sequences of transactions to carry out work. * - * @template ResultFormat - * - * @extends TransactionInterface + * @extends TransactionInterface */ interface SessionInterface extends TransactionInterface { /** * @param iterable $statements - * - * @throws Neo4jException - * - * @return CypherList */ - public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList; + public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): ArrayList; - /** - * @return ResultFormat - */ - public function runStatement(Statement $statement, ?TransactionConfiguration $config = null); + public function runStatement(Statement $statement, ?TransactionConfiguration $config = null): SummarizedResult; - /** - * @param iterable $parameters - * - * @return ResultFormat - */ - public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null); + public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null): SummarizedResult; /** * @psalm-param iterable|null $statements * * @throws Neo4jException * - * @return UnmanagedTransactionInterface + * @return UnmanagedTransactionInterface */ public function beginTransaction(?iterable $statements = null, ?TransactionConfiguration $config = null): UnmanagedTransactionInterface; /** * @template HandlerResult * - * @param callable(TransactionInterface):HandlerResult $tsxHandler + * @param callable(TransactionInterface):HandlerResult $tsxHandler * * @return HandlerResult */ - public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null); + public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null): mixed; /** * @template HandlerResult * - * @param callable(TransactionInterface):HandlerResult $tsxHandler + * @param callable(TransactionInterface):HandlerResult $tsxHandler * * @return HandlerResult */ - public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null); + public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null): mixed; /** * @template HandlerResult * - * @param callable(TransactionInterface):HandlerResult $tsxHandler + * @param callable(TransactionInterface):HandlerResult $tsxHandler * * @return HandlerResult */ - public function transaction(callable $tsxHandler, ?TransactionConfiguration $config = null); + public function transaction(callable $tsxHandler, ?TransactionConfiguration $config = null): mixed; public function getLastBookmark(): Bookmark; } diff --git a/src/Contracts/TransactionInterface.php b/src/Contracts/TransactionInterface.php index d4c77ed0..6813f453 100644 --- a/src/Contracts/TransactionInterface.php +++ b/src/Contracts/TransactionInterface.php @@ -14,14 +14,13 @@ namespace Laudis\Neo4j\Contracts; use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Exception\Neo4jException; -use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\ArrayList; /** * 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 @@ -29,21 +28,21 @@ interface TransactionInterface /** * @param iterable $parameters * - * @return ResultFormat + * @throws Neo4jException */ - public function run(string $statement, iterable $parameters = []); + public function run(string $statement, iterable $parameters = []): SummarizedResult; /** - * @return ResultFormat + * @throws Neo4jException */ - public function runStatement(Statement $statement); + public function runStatement(Statement $statement): SummarizedResult; /** * @param iterable $statements * * @throws Neo4jException * - * @return CypherList + * @return ArrayList */ - public function runStatements(iterable $statements): CypherList; + public function runStatements(iterable $statements): ArrayList; } diff --git a/src/Contracts/UnmanagedTransactionInterface.php b/src/Contracts/UnmanagedTransactionInterface.php index 25f19e65..20f706e0 100644 --- a/src/Contracts/UnmanagedTransactionInterface.php +++ b/src/Contracts/UnmanagedTransactionInterface.php @@ -14,15 +14,13 @@ namespace Laudis\Neo4j\Contracts; use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Types\ArrayList; use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; /** * 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 @@ -32,9 +30,9 @@ interface UnmanagedTransactionInterface extends TransactionInterface * * @param iterable $statements * - * @return CypherList + * @return ArrayList */ - public function commit(iterable $statements = []): CypherList; + public function commit(iterable $statements = []): ArrayList; /** * Rolls back the transaction. diff --git a/src/Neo4j/Neo4jConnectionPool.php b/src/Neo4j/Neo4jConnectionPool.php index 0e116829..c6ce028b 100644 --- a/src/Neo4j/Neo4jConnectionPool.php +++ b/src/Neo4j/Neo4jConnectionPool.php @@ -22,7 +22,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; @@ -205,7 +204,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([ From 274030e1387c509deb7b4fd2dec5e802f966c90e Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 15:06:28 +0530 Subject: [PATCH 06/59] adjusted serverstate typing --- src/Bolt/BoltConnection.php | 2 ++ src/Contracts/ConnectionInterface.php | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 0f3644ba..a5022fb1 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -30,6 +30,7 @@ use Laudis\Neo4j\Enum\ConnectionProtocol; use Laudis\Neo4j\Exception\Neo4jException; use Laudis\Neo4j\Types\CypherList; +use MongoDB\Driver\Server; use Psr\Http\Message\UriInterface; use WeakReference; @@ -322,6 +323,7 @@ private function buildResultExtra(?int $fetchSize, ?int $qid): array public function getServerState(): string { + /** @var ServerState::* */ return $this->boltProtocol->serverState->get(); } diff --git a/src/Contracts/ConnectionInterface.php b/src/Contracts/ConnectionInterface.php index 3d917d99..eec5f6fe 100644 --- a/src/Contracts/ConnectionInterface.php +++ b/src/Contracts/ConnectionInterface.php @@ -13,6 +13,7 @@ namespace Laudis\Neo4j\Contracts; +use Bolt\protocol\ServerState; use Laudis\Neo4j\Databags\DatabaseInfo; use Laudis\Neo4j\Enum\AccessMode; use Laudis\Neo4j\Enum\ConnectionProtocol; @@ -60,6 +61,8 @@ public function getServerVersion(): string; /** * Returns the assumed server state. + * + * @return ServerState::* */ public function getServerState(): string; From 516644679fc4870cc891c4d7f78a83b0866ec926 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 15:42:01 +0530 Subject: [PATCH 07/59] simplified connection --- src/Authentication/Authenticate.php | 2 -- src/Bolt/BoltConnection.php | 15 ++++----------- src/Contracts/ConnectionInterface.php | 5 ----- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/Authentication/Authenticate.php b/src/Authentication/Authenticate.php index 97989c8a..493bbe78 100644 --- a/src/Authentication/Authenticate.php +++ b/src/Authentication/Authenticate.php @@ -23,8 +23,6 @@ * Factory responsible for creating authentication logic. * * @psalm-immutable - * - * @api */ final class Authenticate { diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index a5022fb1..e1121301 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -30,12 +30,11 @@ use Laudis\Neo4j\Enum\ConnectionProtocol; use Laudis\Neo4j\Exception\Neo4jException; use Laudis\Neo4j\Types\CypherList; -use MongoDB\Driver\Server; use Psr\Http\Message\UriInterface; use WeakReference; /** - * @implements ConnectionInterface + * @implements ConnectionInterface * * @psalm-import-type BoltMeta from FormatterInterface * @@ -56,9 +55,9 @@ class BoltConnection implements ConnectionInterface */ private array $subscribedResults = []; - public function getImplementation(): array + public function getImplementation(): V4_4|V5|V5_1|V5_2|V5_3 { - return [$this->boltProtocol, $this->connection]; + return $this->boltProtocol; } /** @@ -66,7 +65,6 @@ public function getImplementation(): array */ public function __construct( private V4_4|V5|V5_1|V5_2|V5_3 $boltProtocol, - private Connection $connection, private AuthenticateInterface $auth, private string $userAgent, /** @psalm-readonly */ @@ -75,7 +73,7 @@ public function __construct( public function getEncryptionLevel(): string { - return $this->connection->getEncryptionLevel(); + return $this->config->getEncryptionLevel(); } /** @@ -139,11 +137,6 @@ public function isOpen(): bool return in_array($this->boltProtocol->serverState->get(), ['DISCONNECTED', 'DEFUNCT'], true); } - public function setTimeout(float $timeout): void - { - $this->connection->setTimeout($timeout); - } - public function consumeResults(): void { foreach ($this->subscribedResults as $result) { diff --git a/src/Contracts/ConnectionInterface.php b/src/Contracts/ConnectionInterface.php index eec5f6fe..25713124 100644 --- a/src/Contracts/ConnectionInterface.php +++ b/src/Contracts/ConnectionInterface.php @@ -92,11 +92,6 @@ public function getDatabaseInfo(): ?DatabaseInfo; */ 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. * From 0eddefbe1eee7cf3d4df2d341c20620a67a30d5b Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 15:52:40 +0530 Subject: [PATCH 08/59] removed fluff from connection --- src/Bolt/BoltConnection.php | 12 ------------ src/Contracts/ConnectionInterface.php | 12 ------------ tests/Unit/Bolt/BoltConnectionTest.php | 10 ++++++++++ 3 files changed, 10 insertions(+), 24 deletions(-) create mode 100644 tests/Unit/Bolt/BoltConnectionTest.php diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index e1121301..56e14fe9 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -65,8 +65,6 @@ public function getImplementation(): V4_4|V5|V5_1|V5_2|V5_3 */ public function __construct( private V4_4|V5|V5_1|V5_2|V5_3 $boltProtocol, - private AuthenticateInterface $auth, - private string $userAgent, /** @psalm-readonly */ private ConnectionConfiguration $config ) {} @@ -124,11 +122,6 @@ public function getDatabaseInfo(): ?DatabaseInfo return $this->config->getDatabaseInfo(); } - public function getAuthentication(): AuthenticateInterface - { - return $this->auth; - } - /** * @psalm-mutation-free */ @@ -325,11 +318,6 @@ public function subscribeResult(CypherList $result): void $this->subscribedResults[] = WeakReference::create($result); } - public function getUserAgent(): string - { - return $this->userAgent; - } - private function assertNoFailure(Response $response): void { if ($response->getSignature() === Response::SIGNATURE_FAILURE) { diff --git a/src/Contracts/ConnectionInterface.php b/src/Contracts/ConnectionInterface.php index 25713124..a897cdf6 100644 --- a/src/Contracts/ConnectionInterface.php +++ b/src/Contracts/ConnectionInterface.php @@ -31,13 +31,6 @@ interface ConnectionInterface */ 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. * @@ -105,9 +98,4 @@ public function isOpen(): bool; * @return ''|'s'|'ssc' */ public function getEncryptionLevel(): string; - - /** - * Returns the user agent handling this connection. - */ - public function getUserAgent(): string; } diff --git a/tests/Unit/Bolt/BoltConnectionTest.php b/tests/Unit/Bolt/BoltConnectionTest.php new file mode 100644 index 00000000..76906976 --- /dev/null +++ b/tests/Unit/Bolt/BoltConnectionTest.php @@ -0,0 +1,10 @@ + Date: Sun, 5 Nov 2023 16:17:35 +0530 Subject: [PATCH 09/59] created run message --- src/Bolt/BoltConnection.php | 7 --- src/Bolt/Messages/Run.php | 82 ++++++++++++++++++++++++++++++ src/Contracts/MessageInterface.php | 17 +++++++ 3 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 src/Bolt/Messages/Run.php create mode 100644 src/Contracts/MessageInterface.php diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 56e14fe9..f8cdc0c6 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -21,9 +21,7 @@ use Bolt\protocol\V5_2; use Bolt\protocol\V5_3; use Laudis\Neo4j\Common\ConnectionConfiguration; -use Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\DatabaseInfo; use Laudis\Neo4j\Enum\AccessMode; @@ -36,8 +34,6 @@ /** * @implements ConnectionInterface * - * @psalm-import-type BoltMeta from FormatterInterface - * * @internal */ class BoltConnection implements ConnectionInterface @@ -195,8 +191,6 @@ public function discard(?int $qid): void * Runs a query/statement. * * Any of the preconditioned states are: 'STREAMING', 'TX_STREAMING', 'FAILED', 'INTERRUPTED'. - * - * @return BoltMeta */ public function run(string $text, array $parameters, ?string $database, ?float $timeout, BookmarkHolder $holder): array { @@ -206,7 +200,6 @@ public function run(string $text, array $parameters, ?string $database, ?float $ $this->assertNoFailure($response); - /** @var BoltMeta */ return $response->getContent(); } diff --git a/src/Bolt/Messages/Run.php b/src/Bolt/Messages/Run.php new file mode 100644 index 00000000..57b294fe --- /dev/null +++ b/src/Bolt/Messages/Run.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\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 Laudis\Neo4j\Contracts\MessageInterface; +use Laudis\Neo4j\Databags\Bookmark; + +/** + * @psalm-readonly + * + * @internal + */ +class Run implements MessageInterface +{ + /** + * @param array $parameters + * @param array $bookmarks + * @param array $txMetadata + * @param list $notificationsDisabledCategories + */ + public function __construct( + private string $text, + private array $parameters = [], + private array $bookmarks = [], + private int|null $txTimeout = null, + private array $txMetadata = [], + private string|null $database = null, + private string|null $impersonatedUser = null, + private string|null $notificationsMinimumSeverity = null, + private array $notificationsDisabledCategories = [] + ) {} + + public function send(V4_4|V5|V5_1|V5_2|V5_3 $bolt): void + { + $extra = []; + if ($this->bookmarks !== []) { + $extra['bookmarks'] = $this->bookmarks; + } + + 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; + } + + $bolt->run($this->text, $this->parameters, $extra); + } +} diff --git a/src/Contracts/MessageInterface.php b/src/Contracts/MessageInterface.php new file mode 100644 index 00000000..1ada0d4a --- /dev/null +++ b/src/Contracts/MessageInterface.php @@ -0,0 +1,17 @@ + Date: Sun, 5 Nov 2023 16:34:19 +0530 Subject: [PATCH 10/59] added hello message --- src/Bolt/Messages/Hello.php | 82 +++++++++++++++++++++++++++++++++++++ src/Bolt/Messages/Run.php | 18 ++++---- 2 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 src/Bolt/Messages/Hello.php diff --git a/src/Bolt/Messages/Hello.php b/src/Bolt/Messages/Hello.php new file mode 100644 index 00000000..6835f831 --- /dev/null +++ b/src/Bolt/Messages/Hello.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\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 Laudis\Neo4j\Contracts\MessageInterface; + +/** +* + * @psalm-readonly + * + * @internal + * + * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-hello + */ +class Hello implements MessageInterface +{ + /** + * @param list $patchBolt + * @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 array $auth, + private string|null $userAgent, + private array $patchBolt, + private array $routing, + private string|null $notificationsMinimumSeverity, + private array $notificationsDisabledCategories, + private array $boltAgent, + ) {} + + public function send(V4_4|V5|V5_2|V5_1|V5_3 $bolt): void + { + $extra = [ + 'auth' => $this->auth, + ]; + + if ($this->userAgent !== null) { + $extra['user_agent'] = $this->userAgent; + } + + if ($this->patchBolt !== []) { + $extra['patch_bolt'] = $this->patchBolt; + } + + 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/Run.php b/src/Bolt/Messages/Run.php index 57b294fe..482acf38 100644 --- a/src/Bolt/Messages/Run.php +++ b/src/Bolt/Messages/Run.php @@ -25,6 +25,8 @@ * @psalm-readonly * * @internal + * + * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-run */ class Run implements MessageInterface { @@ -36,14 +38,14 @@ class Run implements MessageInterface */ public function __construct( private string $text, - private array $parameters = [], - private array $bookmarks = [], - private int|null $txTimeout = null, - private array $txMetadata = [], - private string|null $database = null, - private string|null $impersonatedUser = null, - private string|null $notificationsMinimumSeverity = null, - private array $notificationsDisabledCategories = [] + private array $parameters, + private array $bookmarks, + private int|null $txTimeout, + private array $txMetadata, + private string|null $database, + private string|null $impersonatedUser, + private string|null $notificationsMinimumSeverity, + private array $notificationsDisabledCategories ) {} public function send(V4_4|V5|V5_1|V5_2|V5_3 $bolt): void From 7d1364cbeb935da852dc1059baaef6c705af69d4 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 17:03:24 +0530 Subject: [PATCH 11/59] added Logon and Logoff message --- src/Bolt/Messages/Logoff.php | 23 ++++++++++++++++++++ src/Bolt/Messages/Logon.php | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/Bolt/Messages/Logoff.php create mode 100644 src/Bolt/Messages/Logon.php diff --git a/src/Bolt/Messages/Logoff.php b/src/Bolt/Messages/Logoff.php new file mode 100644 index 00000000..cb6d97f7 --- /dev/null +++ b/src/Bolt/Messages/Logoff.php @@ -0,0 +1,23 @@ +getVersion()); + } + + $bolt->logoff(); + } +} diff --git a/src/Bolt/Messages/Logon.php b/src/Bolt/Messages/Logon.php new file mode 100644 index 00000000..e53b3356 --- /dev/null +++ b/src/Bolt/Messages/Logon.php @@ -0,0 +1,41 @@ + + * + * 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 Laudis\Neo4j\Contracts\MessageInterface; +use LogicException; + +class Logon implements MessageInterface +{ + /** + * @param array{scheme: string}&array $auth + */ + public function __construct( + private array $auth + ) {} + + public function send(V4_4|V5|V5_2|V5_1|V5_3 $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); + } +} From 5c8ce16ab1f5cfdbf37a81ba95d1de5ac5ff9f46 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 17:07:34 +0530 Subject: [PATCH 12/59] added Reset message --- src/Bolt/Messages/GoodBye.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/Bolt/Messages/GoodBye.php diff --git a/src/Bolt/Messages/GoodBye.php b/src/Bolt/Messages/GoodBye.php new file mode 100644 index 00000000..5efc6b1f --- /dev/null +++ b/src/Bolt/Messages/GoodBye.php @@ -0,0 +1,23 @@ +goodbye(); + } +} From a520f01320d8d7dfd65bbec154fbc9fb75cfddd1 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 17:09:23 +0530 Subject: [PATCH 13/59] created Reset message --- src/Bolt/Messages/Reset.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Bolt/Messages/Reset.php diff --git a/src/Bolt/Messages/Reset.php b/src/Bolt/Messages/Reset.php new file mode 100644 index 00000000..2a83ce58 --- /dev/null +++ b/src/Bolt/Messages/Reset.php @@ -0,0 +1,22 @@ +reset(); + } +} From 8612ff2f0a97c49f8656558eb640beb747a9e1e8 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 17:11:47 +0530 Subject: [PATCH 14/59] created discard message --- src/Bolt/Messages/Discard.php | 39 +++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/Bolt/Messages/Discard.php diff --git a/src/Bolt/Messages/Discard.php b/src/Bolt/Messages/Discard.php new file mode 100644 index 00000000..58a77ae8 --- /dev/null +++ b/src/Bolt/Messages/Discard.php @@ -0,0 +1,39 @@ + + * + * 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 Laudis\Neo4j\Contracts\MessageInterface; + +/** + * @internal + * + * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-discard + */ +class Discard implements MessageInterface +{ + public function __construct( + private int $n, + private int $qid + ) {} + + public function send(V4_4|V5|V5_2|V5_1|V5_3 $bolt): void + { + $bolt->discard(['n' => $this->n, 'qid' => $this->qid]); + } +} From 1c8a464f0c6c5588ff8a3746ad7bdf4b59c96ef6 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 17:16:11 +0530 Subject: [PATCH 15/59] added Pull message --- src/Bolt/Messages/Discard.php | 9 +++++-- src/Bolt/Messages/Pull.php | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 src/Bolt/Messages/Pull.php diff --git a/src/Bolt/Messages/Discard.php b/src/Bolt/Messages/Discard.php index 58a77ae8..bb98f37c 100644 --- a/src/Bolt/Messages/Discard.php +++ b/src/Bolt/Messages/Discard.php @@ -29,11 +29,16 @@ class Discard implements MessageInterface { public function __construct( private int $n, - private int $qid + private int|null $qid ) {} public function send(V4_4|V5|V5_2|V5_1|V5_3 $bolt): void { - $bolt->discard(['n' => $this->n, 'qid' => $this->qid]); + $extra = ['n' => $this->n]; + if ($this->qid !== null) { + $extra['qid'] = $this->qid; + } + + $bolt->discard($extra); } } diff --git a/src/Bolt/Messages/Pull.php b/src/Bolt/Messages/Pull.php new file mode 100644 index 00000000..52671806 --- /dev/null +++ b/src/Bolt/Messages/Pull.php @@ -0,0 +1,44 @@ + + * + * 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 Laudis\Neo4j\Contracts\MessageInterface; + +/** + * @internal + * + * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-pull + */ +class Pull implements MessageInterface +{ + public function __construct( + private int $n, + private int|null $qid + ) {} + + public function send(V4_4|V5|V5_1|V5_2|V5_3 $bolt): void + { + $extra = ['n' => $this->n]; + if ($this->qid !== null) { + $extra['qid'] = $this->qid; + } + + $bolt->pull($extra); + } +} From 2c13c20737dd5232b7307ec2c714fb188d1da07e Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 17:30:25 +0530 Subject: [PATCH 16/59] added Begin message and consolidated transaction logic in base class --- src/Bolt/Messages/Begin.php | 17 +++++ src/Bolt/Messages/Run.php | 53 ++++---------- src/Bolt/Messages/TransactionMessage.php | 91 ++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 39 deletions(-) create mode 100644 src/Bolt/Messages/Begin.php create mode 100644 src/Bolt/Messages/TransactionMessage.php diff --git a/src/Bolt/Messages/Begin.php b/src/Bolt/Messages/Begin.php new file mode 100644 index 00000000..eab43ae3 --- /dev/null +++ b/src/Bolt/Messages/Begin.php @@ -0,0 +1,17 @@ +begin($extra); + } +} diff --git a/src/Bolt/Messages/Run.php b/src/Bolt/Messages/Run.php index 482acf38..c0b331f7 100644 --- a/src/Bolt/Messages/Run.php +++ b/src/Bolt/Messages/Run.php @@ -20,6 +20,7 @@ use Bolt\protocol\V5_3; use Laudis\Neo4j\Contracts\MessageInterface; use Laudis\Neo4j\Databags\Bookmark; +use Laudis\Neo4j\Enum\AccessMode; /** * @psalm-readonly @@ -28,7 +29,7 @@ * * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-run */ -class Run implements MessageInterface +class Run extends TransactionMessage { /** * @param array $parameters @@ -39,46 +40,20 @@ class Run implements MessageInterface public function __construct( private string $text, private array $parameters, - private array $bookmarks, - private int|null $txTimeout, - private array $txMetadata, - private string|null $database, - private string|null $impersonatedUser, - private string|null $notificationsMinimumSeverity, - private array $notificationsDisabledCategories - ) {} + array $bookmarks, + int|null $txTimeout, + array $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 send(V4_4|V5|V5_1|V5_2|V5_3 $bolt): void + public function sendWithPreDecoratedExtraData(array $extra, V4_4|V5|V5_1|V5_2|V5_3 $bolt): void { - $extra = []; - if ($this->bookmarks !== []) { - $extra['bookmarks'] = $this->bookmarks; - } - - 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; - } - $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..798d21d3 --- /dev/null +++ b/src/Bolt/Messages/TransactionMessage.php @@ -0,0 +1,91 @@ + + * + * 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 Laudis\Neo4j\Databags\Bookmark; +use Laudis\Neo4j\Enum\AccessMode; + +/** + * @mixin Run + */ +abstract class TransactionMessage +{ + /** + * @param array $bookmarks + * @param array $txMetadata + * @param list $notificationsDisabledCategories + */ + public function __construct( + private array $bookmarks, + private int|null $txTimeout, + private array $txMetadata, + private AccessMode|null $mode, + private string|null $database, + private string|null $impersonatedUser, + private string|null $notificationsMinimumSeverity, + private array $notificationsDisabledCategories + ) {} + + abstract protected function sendWithPreDecoratedExtraData(array $extra, V4_4|V5|V5_1|V5_2|V5_3 $bolt): void; + + public function send(V4_4|V5|V5_1|V5_2|V5_3 $bolt): void + { + $extra = $this->basicTransactionExtra(); + + $this->sendWithPreDecoratedExtraData($extra, $bolt); + } + + private function basicTransactionExtra(): array + { + $extra = []; + if ($this->bookmarks !== []) { + $extra['bookmarks'] = $this->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; + } +} From 755effe0e74653db83d5441df87b0055613c39c0 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 17:32:00 +0530 Subject: [PATCH 17/59] created commit and rollback message --- src/Bolt/Messages/Commit.php | 18 ++++++++++++++++++ src/Bolt/Messages/Rollback.php | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 src/Bolt/Messages/Commit.php create mode 100644 src/Bolt/Messages/Rollback.php diff --git a/src/Bolt/Messages/Commit.php b/src/Bolt/Messages/Commit.php new file mode 100644 index 00000000..35ad758d --- /dev/null +++ b/src/Bolt/Messages/Commit.php @@ -0,0 +1,18 @@ +commit(); + } +} diff --git a/src/Bolt/Messages/Rollback.php b/src/Bolt/Messages/Rollback.php new file mode 100644 index 00000000..7aea16c7 --- /dev/null +++ b/src/Bolt/Messages/Rollback.php @@ -0,0 +1,18 @@ +rollback(); + } +} From a9ceef511aad238dcf4700f2d79bdb36f1ac289b Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 17:37:03 +0530 Subject: [PATCH 18/59] added route message --- src/Bolt/Messages/Route.php | 51 +++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/Bolt/Messages/Route.php diff --git a/src/Bolt/Messages/Route.php b/src/Bolt/Messages/Route.php new file mode 100644 index 00000000..224f97c6 --- /dev/null +++ b/src/Bolt/Messages/Route.php @@ -0,0 +1,51 @@ + + * + * 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 Laudis\Neo4j\Contracts\MessageInterface; + +/** + * @internal + * + * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-route + */ +class Route implements MessageInterface +{ + public function __construct( + private array $routing, + private array $bookmarks, + private string|null $database, + private string|null $impersonatedUser, + ) {} + + public function send(V4_4|V5|V5_2|V5_1|V5_3 $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); + } +} From 7fa13f2ff7fb8869c9d119ffa41dd9380f6de0b9 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 17:47:28 +0530 Subject: [PATCH 19/59] there is noting to patch in supported bolt versions --- src/Bolt/Messages/Hello.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Bolt/Messages/Hello.php b/src/Bolt/Messages/Hello.php index 6835f831..23880de0 100644 --- a/src/Bolt/Messages/Hello.php +++ b/src/Bolt/Messages/Hello.php @@ -31,7 +31,6 @@ class Hello implements MessageInterface { /** - * @param list $patchBolt * @param list $routing * @param list $notificationsDisabledCategories * @param array{scheme: string}&array $auth @@ -40,7 +39,6 @@ class Hello implements MessageInterface public function __construct( private array $auth, private string|null $userAgent, - private array $patchBolt, private array $routing, private string|null $notificationsMinimumSeverity, private array $notificationsDisabledCategories, @@ -57,10 +55,6 @@ public function send(V4_4|V5|V5_2|V5_1|V5_3 $bolt): void $extra['user_agent'] = $this->userAgent; } - if ($this->patchBolt !== []) { - $extra['patch_bolt'] = $this->patchBolt; - } - if ($this->routing !== []) { $extra['routing'] = $this->routing; } From 43501b0cba53cc3f7ecf6c4cb9b101d7c0a2a0f0 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 17:49:43 +0530 Subject: [PATCH 20/59] removed protocol implementation exposure --- src/Bolt/BoltConnection.php | 5 ----- src/Contracts/ConnectionInterface.php | 5 ----- 2 files changed, 10 deletions(-) diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index f8cdc0c6..480498ba 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -51,11 +51,6 @@ class BoltConnection implements ConnectionInterface */ private array $subscribedResults = []; - public function getImplementation(): V4_4|V5|V5_1|V5_2|V5_3 - { - return $this->boltProtocol; - } - /** * @psalm-mutation-free */ diff --git a/src/Contracts/ConnectionInterface.php b/src/Contracts/ConnectionInterface.php index a897cdf6..efad76a2 100644 --- a/src/Contracts/ConnectionInterface.php +++ b/src/Contracts/ConnectionInterface.php @@ -26,11 +26,6 @@ */ interface ConnectionInterface { - /** - * @return ProtocolImplementation - */ - public function getImplementation(); - /** * Returns the agent the servers uses to identify itself. * From 2fa72a7526e026fb33109dc1b1ce9e65cf14ee48 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sun, 5 Nov 2023 17:50:27 +0530 Subject: [PATCH 21/59] fixed flag bug in connection --- src/Bolt/BoltConnection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 480498ba..3fa432c4 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -118,7 +118,7 @@ public function getDatabaseInfo(): ?DatabaseInfo */ public function isOpen(): bool { - return in_array($this->boltProtocol->serverState->get(), ['DISCONNECTED', 'DEFUNCT'], true); + return !in_array($this->boltProtocol->serverState->get(), ['DISCONNECTED', 'DEFUNCT'], true); } public function consumeResults(): void From d25a9e7419dac74b2a5066daa1b807b36fc5a184 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Mon, 4 Dec 2023 00:47:45 -0500 Subject: [PATCH 22/59] temp --- src/Bolt/BoltConnection.php | 135 ++---------------------------------- 1 file changed, 4 insertions(+), 131 deletions(-) diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 3fa432c4..84b096b0 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -22,7 +22,7 @@ use Bolt\protocol\V5_3; use Laudis\Neo4j\Common\ConnectionConfiguration; use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Databags\BookmarkHolder; +use Laudis\Neo4j\Contracts\MessageInterface; use Laudis\Neo4j\Databags\DatabaseInfo; use Laudis\Neo4j\Enum\AccessMode; use Laudis\Neo4j\Enum\ConnectionProtocol; @@ -149,108 +149,17 @@ public function reset(): void $this->subscribedResults = []; } - /** - * Begins a transaction. - * - * Any of the preconditioned states are: 'READY', 'INTERRUPTED'. - */ - public function begin(?string $database, ?float $timeout, BookmarkHolder $holder): void - { - $this->consumeResults(); - $extra = $this->buildRunExtra($database, $timeout, $holder); - - $response = $this->boltProtocol - ->begin($extra) - ->getResponse(); - - $this->assertNoFailure($response); - } - - /** - * Discards a result. - * - * Any of the preconditioned states are: 'STREAMING', 'TX_STREAMING', 'FAILED', 'INTERRUPTED'. - */ - public function discard(?int $qid): void + public function write(MessageInterface $message): array { - $extra = $this->buildResultExtra(null, $qid); - $bolt = $this->boltProtocol; + $message->send($this->boltProtocol); - $response = $bolt->discard($extra) - ->getResponse(); - - $this->assertNoFailure($response); - } - - /** - * Runs a query/statement. - * - * Any of the preconditioned states are: 'STREAMING', 'TX_STREAMING', 'FAILED', 'INTERRUPTED'. - */ - public function run(string $text, array $parameters, ?string $database, ?float $timeout, BookmarkHolder $holder): array - { - $extra = $this->buildRunExtra($database, $timeout, $holder); - $response = $this->boltProtocol->run($text, $parameters, $extra) - ->getResponse(); + $response = $this->boltProtocol->getResponse(); $this->assertNoFailure($response); return $response->getContent(); } - /** - * Commits a transaction. - * - * Any of the preconditioned states are: 'TX_READY', 'INTERRUPTED'. - */ - public function commit(): void - { - $this->consumeResults(); - $response = $this->boltProtocol - ->commit() - ->getResponse(); - - $this->assertNoFailure($response); - } - - /** - * Rolls back a transaction. - * - * Any of the preconditioned states are: 'TX_READY', 'INTERRUPTED'. - */ - public function rollback(): void - { - $this->consumeResults(); - $response = $this->boltProtocol - ->rollback() - ->getResponse(); - - $this->assertNoFailure($response); - } - - /** - * 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 = []; - /** @var Response $response */ - foreach ($this->boltProtocol->pull($extra)->getResponses() as $response) { - $this->assertNoFailure($response); - - $tbr[] = $response->getContent(); - } - - /** @var non-empty-list */ - return $tbr; - } - public function __destruct() { if (!$this->boltProtocol->serverState->is(ServerState::FAILED) && $this->isOpen()) { @@ -264,48 +173,12 @@ public function __destruct() } } - private function buildRunExtra(?string $database, ?float $timeout, BookmarkHolder $holder): array - { - $extra = []; - if ($database) { - $extra['db'] = $database; - } - if ($timeout) { - $extra['tx_timeout'] = (int) ($timeout * 1000); - } - - if (!$holder->getBookmark()->isEmpty()) { - $extra['bookmarks'] = $holder->getBookmark()->values(); - } - - return $extra; - } - - 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 { /** @var ServerState::* */ return $this->boltProtocol->serverState->get(); } - public function subscribeResult(CypherList $result): void - { - $this->subscribedResults[] = WeakReference::create($result); - } - private function assertNoFailure(Response $response): void { if ($response->getSignature() === Response::SIGNATURE_FAILURE) { From 700d332fc3ccaa08bc182bdf0ca077a359205014 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 15:41:56 +0530 Subject: [PATCH 23/59] fixed unit tests using bolt 7.0 --- composer.json | 2 +- src/Bolt/BoltConnection.php | 4 ++-- tests/Unit/BoltFactoryTest.php | 19 ++++++++++++------- tests/Unit/ParameterHelperTest.php | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 4d0146ae..b350f5c2 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "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", "symfony/polyfill-php80": "^1.2", "psr/simple-cache": ">=2.0", "ext-json": "*", diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 66b45f8d..af2f5b0b 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -14,7 +14,7 @@ namespace Laudis\Neo4j\Bolt; use Bolt\protocol\Response; -use Bolt\protocol\ServerState; +use Bolt\enum\ServerState; use Bolt\protocol\V4_4; use Bolt\protocol\V5; use Laudis\Neo4j\Common\ConnectionConfiguration; @@ -282,7 +282,7 @@ public function pull(?int $qid, ?int $fetchSize): array public function __destruct() { - if (!$this->protocol()->serverState->is(ServerState::FAILED) && $this->isOpen()) { + if (!$this->protocol()->serverState === ServerState::FAILED && $this->isOpen()) { if ($this->protocol()->serverState->is(ServerState::STREAMING, ServerState::TX_STREAMING)) { $this->consumeResults(); } diff --git a/tests/Unit/BoltFactoryTest.php b/tests/Unit/BoltFactoryTest.php index a0db0dc7..6a24a3e6 100644 --- a/tests/Unit/BoltFactoryTest.php +++ b/tests/Unit/BoltFactoryTest.php @@ -14,9 +14,7 @@ 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\enum\ServerState; use Bolt\protocol\V5; use Laudis\Neo4j\Authentication\Authenticate; use Laudis\Neo4j\Bolt\BoltConnection; @@ -41,12 +39,19 @@ protected function setUp(): void $basicConnectionFactory = $this->createMock(BasicConnectionFactoryInterface::class); $basicConnectionFactory->method('create') ->willReturn(new Connection($this->createMock(IConnection::class), '')); + + $protocolFactory = $this->createMock(ProtocolFactory::class); $protocolFactory->method('createProtocol') - ->willReturnCallback(static fn (IConnection $connection) => [ - new V5(new Packer(), new Unpacker(), $connection, new ServerState()), - ['server' => 'abc', 'connection_id' => 'i'], - ]); + ->willReturnCallback(static function (IConnection $connection) { + $protocol = new V5(1, $connection); + $protocol->serverState = ServerState::READY; + + return [ + $protocol, + ['server' => 'abc', 'connection_id' => 'i'], + ]; + }); $this->factory = new BoltFactory( $basicConnectionFactory, diff --git a/tests/Unit/ParameterHelperTest.php b/tests/Unit/ParameterHelperTest.php index 28c1a25d..ae4e35e8 100644 --- a/tests/Unit/ParameterHelperTest.php +++ b/tests/Unit/ParameterHelperTest.php @@ -162,7 +162,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 From fd20fac92eda743d9290acad087568fd5c725e8b Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 16:05:52 +0530 Subject: [PATCH 24/59] finish authentication --- phpunit.xml.dist | 2 +- src/Authentication/BasicAuth.php | 34 +++++++++++++++++++----- src/Authentication/KerberosAuth.php | 4 +-- src/Authentication/NoAuth.php | 4 +-- src/Authentication/OpenIDConnectAuth.php | 4 +-- src/Contracts/AuthenticateInterface.php | 6 ++++- 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f83458db..7da76d3d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,6 +15,6 @@ - + diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index 4456925b..bac716af 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -15,10 +15,13 @@ use function base64_encode; -use Bolt\helpers\Auth; -use Bolt\protocol\Response; +use Bolt\enum\Signature; 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\Contracts\AuthenticateInterface; use Laudis\Neo4j\Exception\Neo4jException; @@ -57,15 +60,34 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s /** * @throws Exception */ - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array + public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { - $response = $bolt->hello(Auth::basic($this->username, $this->password, $userAgent)); - if ($response->getSignature() === Response::SIGNATURE_FAILURE) { + if (method_exists($protocol, 'logon')) { + $response = $protocol->hello(['user_agent' => $userAgent])->getResponse(); + if ($response->signature === Signature::FAILURE) { + throw Neo4jException::fromBoltResponse($response); + } + + $response = $protocol->logon([ + 'scheme' => 'basic', + 'principal' => $this->username, + 'credentials' => $this->password, + ])->getResponse(); + } else { + $response = $protocol->hello([ + 'user_agent' => $userAgent, + 'scheme' => 'basic', + 'principal' => $this->username, + 'credentials' => $this->password, + ])->getResponse(); + } + + if ($response->signature === Signature::FAILURE) { throw Neo4jException::fromBoltResponse($response); } /** @var array{server: string, connection_id: string, hints: list} */ - return $response->getContent(); + return $response->content; } public function toString(UriInterface $uri): string diff --git a/src/Authentication/KerberosAuth.php b/src/Authentication/KerberosAuth.php index e1b66f2e..2b63c222 100644 --- a/src/Authentication/KerberosAuth.php +++ b/src/Authentication/KerberosAuth.php @@ -50,9 +50,9 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s ->withHeader('User-Agent', $userAgent); } - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array + public function authenticateBolt(V4_4|V5 $protocol, string $userAgent): array { - $response = $bolt->hello(Auth::kerberos($this->token, $userAgent)); + $response = $protocol->hello(Auth::kerberos($this->token, $userAgent)); if ($response->getSignature() === Response::SIGNATURE_FAILURE) { throw Neo4jException::fromBoltResponse($response); } diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php index e6391f69..32e1c92e 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -42,9 +42,9 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s return $request->withHeader('User-Agent', $userAgent); } - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array + public function authenticateBolt(V4_4|V5 $protocol, string $userAgent): array { - $response = $bolt->hello(Auth::none($userAgent)); + $response = $protocol->hello(Auth::none($userAgent)); if ($response->getSignature() === Response::SIGNATURE_FAILURE) { throw Neo4jException::fromBoltResponse($response); } diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php index a28718a7..15a3e605 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -47,9 +47,9 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s ->withHeader('User-Agent', $userAgent); } - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array + public function authenticateBolt(V4_4|V5 $protocol, string $userAgent): array { - $response = $bolt->hello(Auth::bearer($this->token, $userAgent)); + $response = $protocol->hello(Auth::bearer($this->token, $userAgent)); if ($response->getSignature() === Response::SIGNATURE_FAILURE) { throw Neo4jException::fromBoltResponse($response); } diff --git a/src/Contracts/AuthenticateInterface.php b/src/Contracts/AuthenticateInterface.php index 428dbd4b..ba36ca14 100644 --- a/src/Contracts/AuthenticateInterface.php +++ b/src/Contracts/AuthenticateInterface.php @@ -15,6 +15,10 @@ 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 Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; @@ -32,7 +36,7 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5 $bolt, string $userAgent): array; + public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array; /** * Returns a string representation of the authentication. From 5079e2c926c25344ee3508126c1b05945dfbe37d Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 16:51:19 +0530 Subject: [PATCH 25/59] fix psalm authentication --- psalm-baseline.xml | 181 ------------------ psalm.xml | 3 +- src/Authentication/Authenticate.php | 4 +- src/Authentication/BasicAuth.php | 21 +- src/Authentication/KerberosAuth.php | 31 ++- src/Authentication/NoAuth.php | 27 ++- src/Authentication/OpenIDConnectAuth.php | 29 ++- src/Bolt/BoltConnection.php | 18 +- src/Bolt/Session.php | 2 +- src/Common/ResponseHelper.php | 37 ++++ src/Databags/Neo4jError.php | 2 +- src/Formatter/BasicFormatter.php | 4 +- .../Specialised/BoltOGMTranslator.php | 82 ++++---- src/Neo4j/Neo4jConnectionPool.php | 2 +- src/Types/Map.php | 5 +- 15 files changed, 174 insertions(+), 274 deletions(-) delete mode 100644 psalm-baseline.xml create mode 100644 src/Common/ResponseHelper.php 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..fb61f69c 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="false" > diff --git a/src/Authentication/Authenticate.php b/src/Authentication/Authenticate.php index d0504cac..34b1a287 100644 --- a/src/Authentication/Authenticate.php +++ b/src/Authentication/Authenticate.php @@ -82,7 +82,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 bac716af..4cbf4736 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -13,6 +13,7 @@ namespace Laudis\Neo4j\Authentication; +use Laudis\Neo4j\Common\ResponseHelper; use function base64_encode; use Bolt\enum\Signature; @@ -63,31 +64,25 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { if (method_exists($protocol, 'logon')) { - $response = $protocol->hello(['user_agent' => $userAgent])->getResponse(); - if ($response->signature === Signature::FAILURE) { - throw Neo4jException::fromBoltResponse($response); - } + $protocol->hello(['user_agent' => $userAgent]); + ResponseHelper::getResponse($protocol); - $response = $protocol->logon([ + $protocol->logon([ 'scheme' => 'basic', 'principal' => $this->username, 'credentials' => $this->password, - ])->getResponse(); + ]); } else { - $response = $protocol->hello([ + $protocol->hello([ 'user_agent' => $userAgent, 'scheme' => 'basic', 'principal' => $this->username, 'credentials' => $this->password, - ])->getResponse(); - } - - if ($response->signature === Signature::FAILURE) { - throw Neo4jException::fromBoltResponse($response); + ]); } /** @var array{server: string, connection_id: string, hints: list} */ - return $response->content; + return ResponseHelper::getResponse($protocol)->content; } public function toString(UriInterface $uri): string diff --git a/src/Authentication/KerberosAuth.php b/src/Authentication/KerberosAuth.php index 2b63c222..849fde27 100644 --- a/src/Authentication/KerberosAuth.php +++ b/src/Authentication/KerberosAuth.php @@ -13,12 +13,14 @@ 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; @@ -50,15 +52,28 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s ->withHeader('User-Agent', $userAgent); } - public function authenticateBolt(V4_4|V5 $protocol, string $userAgent): array + public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { - $response = $protocol->hello(Auth::kerberos($this->token, $userAgent)); - if ($response->getSignature() === Response::SIGNATURE_FAILURE) { - throw Neo4jException::fromBoltResponse($response); + if (method_exists($protocol, 'logon')) { + $protocol->hello(['user_agent' => $userAgent]); + ResponseHelper::getResponse($protocol); + + $protocol->logon([ + 'scheme' => 'kerberos', + 'principal' => '', + 'credentials' => $this->token, + ]); + } else { + $protocol->hello([ + 'user_agent' => $userAgent, + 'scheme' => 'kerberos', + 'principal' => '', + 'credentials' => $this->token, + ]); } /** @var array{server: string, connection_id: string, hints: list} */ - return $response->getContent(); + return ResponseHelper::getResponse($protocol); } public function toString(UriInterface $uri): string diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php index 32e1c92e..6fd93a4b 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -13,10 +13,14 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\helpers\Auth; -use Bolt\protocol\Response; +use Bolt\enum\Signature; 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; @@ -42,15 +46,24 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s return $request->withHeader('User-Agent', $userAgent); } - public function authenticateBolt(V4_4|V5 $protocol, string $userAgent): array + public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { - $response = $protocol->hello(Auth::none($userAgent)); - if ($response->getSignature() === Response::SIGNATURE_FAILURE) { - throw Neo4jException::fromBoltResponse($response); + if (method_exists($protocol, 'logon')) { + $protocol->hello(['user_agent' => $userAgent]); + ResponseHelper::getResponse($protocol); + + $protocol->logon([ + 'scheme' => 'none', + ]); + } else { + $protocol->hello([ + 'user_agent' => $userAgent, + 'scheme' => 'none', + ]); } /** @var array{server: string, connection_id: string, hints: list} */ - return $response->getContent(); + return ResponseHelper::getResponse($protocol)->content; } public function toString(UriInterface $uri): string diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php index 15a3e605..9ba89783 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -13,10 +13,14 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\helpers\Auth; -use Bolt\protocol\Response; +use Bolt\enum\Signature; 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; @@ -47,15 +51,26 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s ->withHeader('User-Agent', $userAgent); } - public function authenticateBolt(V4_4|V5 $protocol, string $userAgent): array + public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { - $response = $protocol->hello(Auth::bearer($this->token, $userAgent)); - if ($response->getSignature() === Response::SIGNATURE_FAILURE) { - throw Neo4jException::fromBoltResponse($response); + if (method_exists($protocol, 'logon')) { + $protocol->hello(['user_agent' => $userAgent]); + ResponseHelper::getResponse($protocol); + + $protocol->logon([ + 'scheme' => 'bearer', + 'credentials' => $this->token, + ]); + } else { + $protocol->hello([ + 'user_agent' => $userAgent, + 'scheme' => 'bearer', + 'credentials' => $this->token, + ]); } /** @var array{server: string, connection_id: string, hints: list} */ - return $response->getContent(); + return ResponseHelper::getResponse($protocol)->content; } public function toString(UriInterface $uri): string diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index af2f5b0b..87618736 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -15,6 +15,7 @@ use Bolt\protocol\Response; use Bolt\enum\ServerState; +use Bolt\enum\Signature; use Bolt\protocol\V4_4; use Bolt\protocol\V5; use Laudis\Neo4j\Common\ConnectionConfiguration; @@ -220,7 +221,7 @@ public function run(string $text, array $parameters, ?string $database, ?float $ $this->assertNoFailure($response); /** @var BoltMeta */ - return $response->getContent(); + return $response->content; } /** @@ -270,10 +271,11 @@ public function pull(?int $qid, ?int $fetchSize): array $extra = $this->buildResultExtra($fetchSize, $qid); $tbr = []; + /** @var Response $response */ foreach ($this->protocol()->pull($extra)->getResponses() as $response) { $this->assertNoFailure($response); - $tbr[] = $response->getContent(); + $tbr[] = $response->content; } /** @var non-empty-list */ @@ -282,8 +284,8 @@ public function pull(?int $qid, ?int $fetchSize): array public function __destruct() { - if (!$this->protocol()->serverState === ServerState::FAILED && $this->isOpen()) { - if ($this->protocol()->serverState->is(ServerState::STREAMING, ServerState::TX_STREAMING)) { + if ($this->protocol()->serverState === ServerState::FAILED && $this->isOpen()) { + if ($this->protocol()->serverState === ServerState::STREAMING || $this->protocol()->serverState === ServerState::TX_STREAMING) { $this->consumeResults(); } @@ -296,10 +298,10 @@ public function __destruct() private function buildRunExtra(?string $database, ?float $timeout, BookmarkHolder $holder, ?AccessMode $mode): array { $extra = []; - if ($database) { + if ($database !== null) { $extra['db'] = $database; } - if ($timeout) { + if ($timeout !== null) { $extra['tx_timeout'] = (int) ($timeout * 1000); } @@ -330,7 +332,7 @@ private function buildResultExtra(?int $fetchSize, ?int $qid): array public function getServerState(): string { - return $this->protocol()->serverState->get(); + return $this->protocol()->serverState->name; } public function subscribeResult(CypherList $result): void @@ -345,7 +347,7 @@ public function getUserAgent(): string private function assertNoFailure(Response $response): void { - if ($response->getSignature() === Response::SIGNATURE_FAILURE) { + if ($response->signature === Signature::FAILURE) { throw Neo4jException::fromBoltResponse($response); } } diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php index 9c062b1d..0cf01535 100644 --- a/src/Bolt/Session.php +++ b/src/Bolt/Session.php @@ -150,7 +150,7 @@ private function acquireConnection(TransactionConfiguration $config, SessionConf // 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/Common/ResponseHelper.php b/src/Common/ResponseHelper.php new file mode 100644 index 00000000..9fb6a094 --- /dev/null +++ b/src/Common/ResponseHelper.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Common; + +use Bolt\enum\Signature; +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\Exception\Neo4jException; + +class ResponseHelper +{ + public static function getResponse(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): Response + { + $response = $protocol->getResponse(); + if ($response->signature === Signature::FAILURE) { + throw Neo4jException::fromBoltResponse($response); + } + + return $response; + } +} diff --git a/src/Databags/Neo4jError.php b/src/Databags/Neo4jError.php index bbc7210b..8d220abc 100644 --- a/src/Databags/Neo4jError.php +++ b/src/Databags/Neo4jError.php @@ -39,7 +39,7 @@ public function __construct( public static function fromBoltResponse(Response $response): self { /** @var array{code: string, message:string} $content */ - $content = $response->getContent(); + $content = $response->content; return self::fromMessageAndCode($content['code'], $content['message']); } diff --git a/src/Formatter/BasicFormatter.php b/src/Formatter/BasicFormatter.php index 244e2e5a..0cefc2ed 100644 --- a/src/Formatter/BasicFormatter.php +++ b/src/Formatter/BasicFormatter.php @@ -144,8 +144,8 @@ private function formatRow(array $meta, array $result): CypherMap private function mapPath(Path $path): array { - $relationships = $path->rels(); - $nodes = $path->nodes(); + $relationships = $path->rels; + $nodes = $path->nodes; $tbr = []; /** * @var mixed $node diff --git a/src/Formatter/Specialised/BoltOGMTranslator.php b/src/Formatter/Specialised/BoltOGMTranslator.php index 5ad88ad0..6bda4600 100644 --- a/src/Formatter/Specialised/BoltOGMTranslator.php +++ b/src/Formatter/Specialised/BoltOGMTranslator.php @@ -95,21 +95,21 @@ private function makeFromBoltNode(BoltNode $node): Node * @var string $name * @var mixed $property */ - foreach ($node->properties() as $name => $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(); + $elementId = $node->element_id; } /** * @psalm-suppress MixedArgumentTypeCoercion */ return new Node( - $node->id(), - new CypherList($node->labels()), + $node->id, + new CypherList($node->labels), new CypherMap($properties), $elementId ); @@ -117,50 +117,50 @@ private function makeFromBoltNode(BoltNode $node): Node private function makeFromBoltDate(BoltDate $date): Date { - return new Date($date->days()); + return new Date($date->days); } private function makeFromBoltLocalDateTime(BoltLocalDateTime $time): LocalDateTime { - return new LocalDateTime($time->seconds(), $time->nanoseconds()); + return new LocalDateTime($time->seconds, $time->nanoseconds); } private function makeBoltTimezoneIdentifier(BoltDateTimeZoneId $time): DateTimeZoneId { /** @var non-empty-string $tzId */ - $tzId = $time->tz_id(); + $tzId = $time->tz_id; - return new DateTimeZoneId($time->seconds(), $time->nanoseconds(), $tzId); + 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(), + $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->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()); + return new Time($time->nanoseconds, $time->tz_offset_seconds); } private function makeFromBoltLocalTime(BoltLocalTime $time): LocalTime { - return new LocalTime($time->nanoseconds()); + return new LocalTime($time->nanoseconds); } private function makeFromBoltRelationship(BoltRelationship $rel): Relationship @@ -171,21 +171,21 @@ private function makeFromBoltRelationship(BoltRelationship $rel): Relationship * @var string $key * @var mixed $property */ - foreach ($rel->properties() as $key => $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(); + $elementId = $rel->element_id; } return new Relationship( - $rel->id(), - $rel->startNodeId(), - $rel->endNodeId(), - $rel->type(), + $rel->id, + $rel->startNodeId, + $rel->endNodeId, + $rel->type, new CypherMap($map), $elementId ); @@ -199,18 +199,18 @@ private function makeFromBoltUnboundRelationship(BoltUnboundRelationship $rel): * @var string $key * @var mixed $property */ - foreach ($rel->properties() as $key => $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(); + $elementId = $rel->element_id; } return new UnboundRelationship( - $rel->id(), - $rel->type(), + $rel->id, + $rel->type, new CypherMap($map), $elementId ); @@ -218,40 +218,40 @@ private function makeFromBoltUnboundRelationship(BoltUnboundRelationship $rel): 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()); + 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.'); + 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()); + 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.'); + 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(); + $boltNodes = $path->nodes; foreach ($boltNodes as $node) { $nodes[] = $this->makeFromBoltNode($node); } $relationships = []; /** @var list $rels */ - $rels = $path->rels(); + $rels = $path->rels; foreach ($rels as $rel) { $relationships[] = $this->makeFromBoltUnboundRelationship($rel); } /** @var list $ids */ - $ids = $path->ids(); + $ids = $path->ids; return new Path( new CypherList($nodes), @@ -265,7 +265,7 @@ private function makeFromBoltPath(BoltPath $path): Path */ private function mapArray(array $value): CypherList|CypherMap { - if (array_key_exists(0, $value)) { + if (array_is_list($value)) { /** @var array $vector */ $vector = []; /** @var mixed $x */ diff --git a/src/Neo4j/Neo4jConnectionPool.php b/src/Neo4j/Neo4jConnectionPool.php index d98a23c4..b416dd01 100644 --- a/src/Neo4j/Neo4jConnectionPool.php +++ b/src/Neo4j/Neo4jConnectionPool.php @@ -186,7 +186,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(); diff --git a/src/Types/Map.php b/src/Types/Map.php index d03e432a..00a1bb8a 100644 --- a/src/Types/Map.php +++ b/src/Types/Map.php @@ -190,7 +190,7 @@ 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) { + if ($comparator !== null) { return $comparator($x->getKey(), $y->getKey()); } @@ -245,12 +245,13 @@ public function xor(iterable $map): Map * * @param iterable $values * - * @return static + * @return self * * @psalm-mutation-free */ public function merge(iterable $values): Map { + /** @var self */ return $this->withOperation(function () use ($values) { $tbr = $this->toArray(); $values = Map::fromIterable($values); From ad9b8c580efc1ba9d99480d3c7fe2d218a37df17 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 16:54:23 +0530 Subject: [PATCH 26/59] fixed psalm bolt --- src/Bolt/BoltConnection.php | 2 +- src/Bolt/SslConfigurationFactory.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 87618736..848de6f9 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -136,7 +136,7 @@ public function getAuthentication(): AuthenticateInterface */ public function isOpen(): bool { - return !in_array($this->protocol()->serverState->get(), ['DISCONNECTED', 'DEFUNCT'], true); + return !in_array($this->protocol()->serverState, [ServerState::DISCONNECTED, ServerState::DEFUNCT], true); } public function setTimeout(float $timeout): void diff --git a/src/Bolt/SslConfigurationFactory.php b/src/Bolt/SslConfigurationFactory.php index d4f94258..42fcbee6 100644 --- a/src/Bolt/SslConfigurationFactory.php +++ b/src/Bolt/SslConfigurationFactory.php @@ -31,10 +31,12 @@ class SslConfigurationFactory public function create(UriInterface $uri, SslConfiguration $config): array { $mode = $config->getMode(); + /** @var ''|'s'|'ssc' $sslConfig */ $sslConfig = ''; if ($mode === SslMode::FROM_URL()) { $scheme = $uri->getScheme(); $explosion = explode('+', $scheme, 2); + /** @var ''|'s'|'ssc' $sslConfig */ $sslConfig = $explosion[1] ?? ''; } elseif ($mode === SslMode::ENABLE()) { $sslConfig = 's'; @@ -46,7 +48,7 @@ public function create(UriInterface $uri, SslConfiguration $config): array return [$sslConfig, $this->enableSsl($uri->getHost(), $sslConfig, $config)]; } - return [$sslConfig, []]; + return ['', []]; } /** From 38de44861ed6cfff26e9712a6877b871a3ca5a60 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 16:57:33 +0530 Subject: [PATCH 27/59] fix psalm common --- src/Common/GeneratorHelper.php | 2 +- src/Common/SysVSemaphore.php | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) 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/SysVSemaphore.php b/src/Common/SysVSemaphore.php index fa2e0f0e..0845fa21 100644 --- a/src/Common/SysVSemaphore.php +++ b/src/Common/SysVSemaphore.php @@ -29,11 +29,8 @@ class SysVSemaphore implements SemaphoreInterface { - /** - * @param resource $semaphore - */ private function __construct( - private $semaphore + private readonly \SysvSemaphore $semaphore ) {} public static function create(string $key, int $max): self @@ -41,7 +38,16 @@ public static function create(string $key, int $max): self $key = hash('sha512', $key, true); $key = substr($key, 0, 8); - return new self(sem_get(hexdec($key), $max)); + if (!function_exists('sem_get')) { + throw new RuntimeException('Can only create a semaphore if the sysv extension is installed'); + } + + $semaphore = sem_get(hexdec($key), $max); + if ($semaphore === false) { + throw new RuntimeException('Could not create semaphore'); + } + + return new self($semaphore); } public function wait(): Generator From d0278a691ea2befc5197fb5b84a9ee2566acdd34 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 17:34:59 +0530 Subject: [PATCH 28/59] psalm databags --- src/Databags/DatabaseInfo.php | 2 ++ src/Databags/Neo4jError.php | 5 ++++- src/Databags/Plan.php | 2 ++ src/Databags/ProfiledPlan.php | 4 +++- src/Databags/ResultSummary.php | 2 ++ src/Databags/ServerInfo.php | 2 ++ src/Databags/Statement.php | 2 ++ src/Databags/SummaryCounters.php | 4 +++- src/Databags/TransactionConfiguration.php | 4 ++-- 9 files changed, 22 insertions(+), 5 deletions(-) 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/Neo4jError.php b/src/Databags/Neo4jError.php index 8d220abc..cebe1ae3 100644 --- a/src/Databags/Neo4jError.php +++ b/src/Databags/Neo4jError.php @@ -38,7 +38,10 @@ public function __construct( */ public static function fromBoltResponse(Response $response): self { - /** @var array{code: string, message:string} $content */ + /** + * @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/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/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..c1dacf0e 100644 --- a/src/Databags/TransactionConfiguration.php +++ b/src/Databags/TransactionConfiguration.php @@ -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); } From ec9435e7cf347a5e9ccba4203fc5e9bfd8647d09 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 17:56:38 +0530 Subject: [PATCH 29/59] psalm formatter --- src/Contracts/FormatterInterface.php | 3 ++- src/Formatter/BasicFormatter.php | 4 ++-- src/Formatter/Specialised/BoltOGMTranslator.php | 2 ++ .../Specialised/JoltHttpOGMTranslator.php | 2 ++ .../Specialised/LegacyHttpOGMTranslator.php | 17 ++++++++++++++--- src/Formatter/SummarizedResultFormatter.php | 3 +-- 6 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/Contracts/FormatterInterface.php b/src/Contracts/FormatterInterface.php index ebaaa27b..ded59d2e 100644 --- a/src/Contracts/FormatterInterface.php +++ b/src/Contracts/FormatterInterface.php @@ -58,7 +58,8 @@ * constraints-removed?: int, * contains-updates?: bool, * contains-system-updates?: bool, - * system-updates?: int + * system-updates?: int, + * db?: string * } * @psalm-type CypherError = array{code: string, message: string} * @psalm-type CypherRowResponse = array{row: list>} diff --git a/src/Formatter/BasicFormatter.php b/src/Formatter/BasicFormatter.php index 0cefc2ed..766a0d13 100644 --- a/src/Formatter/BasicFormatter.php +++ b/src/Formatter/BasicFormatter.php @@ -58,7 +58,7 @@ public static function create(): self } /** - * @param array{fields: array} $meta + * @param array{fields: array, qid?: int, t_first: int} $meta * * @return CypherList> */ @@ -127,7 +127,7 @@ private function buildResult(stdClass $result): CypherList } /** - * @param array{fields: array} $meta + * @param array{fields: array, qid?: int, t_first: int} $meta * * @return CypherMap */ diff --git a/src/Formatter/Specialised/BoltOGMTranslator.php b/src/Formatter/Specialised/BoltOGMTranslator.php index 6bda4600..017b248b 100644 --- a/src/Formatter/Specialised/BoltOGMTranslator.php +++ b/src/Formatter/Specialised/BoltOGMTranslator.php @@ -54,6 +54,7 @@ * @psalm-import-type OGMTypes from OGMFormatter * * @psalm-immutable + * @psalm-pure */ final class BoltOGMTranslator { @@ -64,6 +65,7 @@ final class BoltOGMTranslator public function __construct() { + /** @psalm-suppress InvalidPropertyAssignmentValue */ $this->rawToTypes = [ BoltNode::class => $this->makeFromBoltNode(...), BoltDate::class => $this->makeFromBoltDate(...), diff --git a/src/Formatter/Specialised/JoltHttpOGMTranslator.php b/src/Formatter/Specialised/JoltHttpOGMTranslator.php index 62d8b04c..dadac1ba 100644 --- a/src/Formatter/Specialised/JoltHttpOGMTranslator.php +++ b/src/Formatter/Specialised/JoltHttpOGMTranslator.php @@ -64,6 +64,8 @@ * @psalm-immutable * * @psalm-import-type OGMTypes from OGMFormatter + * + * @psalm-suppress PossiblyUndefinedArrayOffset */ final class JoltHttpOGMTranslator { diff --git a/src/Formatter/Specialised/LegacyHttpOGMTranslator.php b/src/Formatter/Specialised/LegacyHttpOGMTranslator.php index 1997f484..e66a71eb 100644 --- a/src/Formatter/Specialised/LegacyHttpOGMTranslator.php +++ b/src/Formatter/Specialised/LegacyHttpOGMTranslator.php @@ -154,15 +154,18 @@ public function translateCypherMap(array $row, HttpMetaInfo $meta): array } /** - * @param stdClass|array|scalar|null $value + * @param array|scalar|stdClass|null $value * * @return array{0: OGMTypes, 1: HttpMetaInfo} * * @psalm-suppress MixedArgumentTypeCoercion * @psalm-suppress MixedArgument * @psalm-suppress MixedAssignment + * @psalm-suppress InvalidReturnStatement + * @psalm-suppress ArgumentTypeCoercion + * @psalm-suppress InvalidReturnType */ - private function translateValue($value, HttpMetaInfo $meta): array + private function translateValue(float|array|bool|int|string|stdClass|null $value, HttpMetaInfo $meta): array { if (is_object($value)) { return $this->translateObject($value, $meta); @@ -416,6 +419,7 @@ private function translateDuration(string $value): Duration { /** @psalm-suppress ImpureFunctionCall false positive in version php 7.4 */ if (str_contains($value, '.')) { + /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ [$format, $secondsFraction] = explode('.', $value); $nanoseconds = (int) substr($secondsFraction, 6); $microseconds = (int) str_pad((string) ((int) substr($secondsFraction, 0, 6)), 6, '0'); @@ -464,14 +468,19 @@ private function translateTime(string $value): Time */ private function translateDateTime(string $value): DateTime { + /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ [$date, $time] = explode('T', $value); $tz = null; - /** @psalm-suppress ImpureFunctionCall false positive in version php 7.4 */ if (str_contains($time, '+')) { + /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ [$time, $timezone] = explode('+', $time); + + /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ [$tzHours, $tzMinutes] = explode(':', $timezone); $tz = (int) $tzHours * 60 * 60 + (int) $tzMinutes * 60; } + + /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ [$time, $milliseconds] = explode('.', $time); $dateTime = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date.' '.$time); @@ -488,7 +497,9 @@ private function translateDateTime(string $value): DateTime private function translateLocalDateTime(string $value): LocalDateTime { + /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ [$date, $time] = explode('T', $value); + /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ [$time, $milliseconds] = explode('.', $time); $dateTime = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date.' '.$time); diff --git a/src/Formatter/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php index e3d6c824..c8864f76 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -129,7 +129,7 @@ public function formatHttpStats(stdClass $response, HttpConnection $connection, } /** - * @param array{stats?: BoltCypherStats} $response + * @param array{stats?: BoltCypherStats}&array $response * * @psalm-mutation-free */ @@ -173,7 +173,6 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection /** @var BoltCypherStats $response */ $stats = $this->formatBoltStats($response); $resultConsumedAfter = microtime(true) - $runStart; - /** @var string */ $db = $response['db'] ?? ''; $summary = new ResultSummary( $stats, From 6d188189adae0b6b91d9d6e7616e49e8e7151d3c Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 18:09:54 +0530 Subject: [PATCH 30/59] psalm http --- src/Types/Abstract3DPoint.php | 1 + src/Types/AbstractCypherSequence.php | 20 +++++++++++++------- src/Types/AbstractPoint.php | 2 ++ src/Types/ArrayList.php | 3 +++ src/Types/Map.php | 3 ++- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Types/Abstract3DPoint.php b/src/Types/Abstract3DPoint.php index a2fad0e3..7bef1a42 100644 --- a/src/Types/Abstract3DPoint.php +++ b/src/Types/Abstract3DPoint.php @@ -48,6 +48,7 @@ public function getZ(): float } /** + * @psalm-suppress ImplementedReturnTypeMismatch * @return array{x: float, y: float, z: float, srid: int, crs: Crs} */ public function toArray(): array diff --git a/src/Types/AbstractCypherSequence.php b/src/Types/AbstractCypherSequence.php index f7271197..c5aeb3e2 100644 --- a/src/Types/AbstractCypherSequence.php +++ b/src/Types/AbstractCypherSequence.php @@ -13,6 +13,7 @@ namespace Laudis\Neo4j\Types; +use Generator; use function array_key_exists; use function array_reverse; @@ -70,20 +71,20 @@ abstract class AbstractCypherSequence implements Countable, JsonSerializable, Ar protected int $generatorPosition = 0; /** - * @var (callable():(\Iterator))|\Iterator + * @var (callable():(Iterator))|Iterator */ protected $generator; /** * @template Value * - * @param callable():(\Generator) $operation + * @param callable():(Generator) $operation * * @return static * * @psalm-mutation-free */ - abstract protected function withOperation($operation): self; + abstract protected function withOperation(callable $operation): self; /** * Copies the sequence. @@ -282,7 +283,7 @@ public function sorted(?callable $comparator = null): self return $this->withOperation(function () use ($comparator) { $iterable = $this->toArray(); - if ($comparator) { + if ($comparator !== null) { uasort($iterable, $comparator); } else { asort($iterable); @@ -298,6 +299,7 @@ public function sorted(?callable $comparator = null): self * @return ArrayList * * @psalm-mutation-free + * @psalm-suppress MixedArrayAccess */ public function pluck(string $key): ArrayList { @@ -318,6 +320,7 @@ public function pluck(string $key): ArrayList * @return Map * * @psalm-mutation-free + * @psalm-suppress MixedArrayAccess */ public function keyBy(string $key): Map { @@ -467,6 +470,7 @@ public function next(): void $generator->next(); if ($generator->valid()) { + /** @var TKey */ $this->keyCache[] = $generator->key(); $this->cache[$generator->key()] = $generator->current(); } @@ -520,14 +524,16 @@ private function setupCache(): void { $generator = $this->getGenerator(); - if (count($this->cache) !== 0 && count($this->cache) % ($this->cacheLimit + 1) === 0) { + if (count($this->keyCache) !== 0 && 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(); + /** @var TKey $key */ + $key = $generator->key(); + $this->cache[$key] = $generator->current(); + $this->keyCache[] = $key; } } diff --git a/src/Types/AbstractPoint.php b/src/Types/AbstractPoint.php index 9a932bc2..ac5da8cf 100644 --- a/src/Types/AbstractPoint.php +++ b/src/Types/AbstractPoint.php @@ -26,6 +26,8 @@ * @psalm-immutable * * @psalm-import-type Crs from PointInterface + * + * @extends AbstractPropertyObject */ abstract class AbstractPoint extends AbstractPropertyObject implements PointInterface, BoltConvertibleInterface { diff --git a/src/Types/ArrayList.php b/src/Types/ArrayList.php index 0738fa55..b41fe92f 100644 --- a/src/Types/ArrayList.php +++ b/src/Types/ArrayList.php @@ -106,6 +106,9 @@ public function last() * * @param iterable $values * + * @psalm-suppress LessSpecificImplementedReturnType + * @psalm-suppress ImplementedReturnTypeMismatch + * * @return static * * @psalm-mutation-free diff --git a/src/Types/Map.php b/src/Types/Map.php index 00a1bb8a..779e995b 100644 --- a/src/Types/Map.php +++ b/src/Types/Map.php @@ -245,13 +245,14 @@ public function xor(iterable $map): Map * * @param iterable $values * + * @psalm-suppress LessSpecificImplementedReturnType + * * @return self * * @psalm-mutation-free */ public function merge(iterable $values): Map { - /** @var self */ return $this->withOperation(function () use ($values) { $tbr = $this->toArray(); $values = Map::fromIterable($values); From b65188e966bfd832a81a1831a67c03d2a74a5cdd Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 18:23:19 +0530 Subject: [PATCH 31/59] psalm fixed --- src/Common/DNSAddressResolver.php | 6 ++++-- src/Contracts/AddressResolverInterface.php | 6 ++++-- tests/Integration/BoltDriverIntegrationTest.php | 2 ++ tests/Unit/CypherListTest.php | 7 +++++-- tests/Unit/CypherMapTest.php | 6 ++++++ tests/Unit/DNSAddressResolverTest.php | 9 +++++---- tests/Unit/ParameterHelperTest.php | 5 ++++- 7 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/Common/DNSAddressResolver.php b/src/Common/DNSAddressResolver.php index 70025b14..04bd36cd 100644 --- a/src/Common/DNSAddressResolver.php +++ b/src/Common/DNSAddressResolver.php @@ -13,6 +13,8 @@ namespace Laudis\Neo4j\Common; +use Generator; +use Traversable; use function array_filter; use function array_map; use function array_unique; @@ -29,9 +31,9 @@ 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/Contracts/AddressResolverInterface.php b/src/Contracts/AddressResolverInterface.php index 77c30ecf..94fe043b 100644 --- a/src/Contracts/AddressResolverInterface.php +++ b/src/Contracts/AddressResolverInterface.php @@ -13,12 +13,14 @@ namespace Laudis\Neo4j\Contracts; +use Generator; + interface AddressResolverInterface { /** * Returns the addresses. * - * @return iterable + * @return Generator */ - public function getAddresses(string $host): iterable; + public function getAddresses(string $host): Generator; } diff --git a/tests/Integration/BoltDriverIntegrationTest.php b/tests/Integration/BoltDriverIntegrationTest.php index fdceec5d..73bd724e 100644 --- a/tests/Integration/BoltDriverIntegrationTest.php +++ b/tests/Integration/BoltDriverIntegrationTest.php @@ -24,6 +24,7 @@ final class BoltDriverIntegrationTest extends EnvironmentAwareIntegrationTest { /** * @throws Exception + * @psalm-suppress MixedMethodCall */ public function testValidHostname(): void { @@ -36,6 +37,7 @@ public function testValidHostname(): void /** * @throws Exception + * @psalm-suppress MixedMethodCall */ public function testValidUrl(): void { 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..ba426412 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('test.ghlen.com')); $this->assertEqualsCanonicalizing(['test.ghlen.com', '123.123.123.123', '123.123.123.124'], $records); $this->assertNotEmpty($records); - $this->assertEquals('test.ghlen.com', $records[0]); + $this->assertEquals('test.ghlen.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')); $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')); + $this->assertEquals('bogus', $addresses); } } diff --git a/tests/Unit/ParameterHelperTest.php b/tests/Unit/ParameterHelperTest.php index ae4e35e8..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; From 43a3a3f8725a199917854d3114ec9af707df6370 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 18:23:42 +0530 Subject: [PATCH 32/59] ran php cs fixer --- src/Authentication/BasicAuth.php | 4 +--- src/Authentication/NoAuth.php | 2 -- src/Authentication/OpenIDConnectAuth.php | 2 -- src/Bolt/BoltConnection.php | 2 +- src/Common/DNSAddressResolver.php | 3 +-- src/Databags/Neo4jError.php | 1 + src/Enum/QueryTypeEnum.php | 3 ++- src/Enum/SslMode.php | 3 ++- src/Formatter/Specialised/BoltOGMTranslator.php | 1 + src/Types/Abstract3DPoint.php | 1 + src/Types/AbstractCypherSequence.php | 4 +++- tests/Integration/BoltDriverIntegrationTest.php | 2 ++ tests/Unit/BoltFactoryTest.php | 1 - 13 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index 4cbf4736..eff1b5a5 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -13,10 +13,8 @@ namespace Laudis\Neo4j\Authentication; -use Laudis\Neo4j\Common\ResponseHelper; use function base64_encode; -use Bolt\enum\Signature; use Bolt\protocol\V4_4; use Bolt\protocol\V5; use Bolt\protocol\V5_1; @@ -24,8 +22,8 @@ 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; diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php index 6fd93a4b..75617fdc 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -13,7 +13,6 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\enum\Signature; use Bolt\protocol\V4_4; use Bolt\protocol\V5; use Bolt\protocol\V5_1; @@ -22,7 +21,6 @@ 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; diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php index 9ba89783..61bb7e42 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -13,7 +13,6 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\enum\Signature; use Bolt\protocol\V4_4; use Bolt\protocol\V5; use Bolt\protocol\V5_1; @@ -22,7 +21,6 @@ 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; diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 848de6f9..e5ee2f6b 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -13,9 +13,9 @@ namespace Laudis\Neo4j\Bolt; -use Bolt\protocol\Response; use Bolt\enum\ServerState; use Bolt\enum\Signature; +use Bolt\protocol\Response; use Bolt\protocol\V4_4; use Bolt\protocol\V5; use Laudis\Neo4j\Common\ConnectionConfiguration; diff --git a/src/Common/DNSAddressResolver.php b/src/Common/DNSAddressResolver.php index 04bd36cd..6a6ac5e5 100644 --- a/src/Common/DNSAddressResolver.php +++ b/src/Common/DNSAddressResolver.php @@ -13,8 +13,6 @@ namespace Laudis\Neo4j\Common; -use Generator; -use Traversable; use function array_filter; use function array_map; use function array_unique; @@ -25,6 +23,7 @@ use function dns_get_record; +use Generator; use Laudis\Neo4j\Contracts\AddressResolverInterface; use Throwable; diff --git a/src/Databags/Neo4jError.php b/src/Databags/Neo4jError.php index cebe1ae3..d592ebed 100644 --- a/src/Databags/Neo4jError.php +++ b/src/Databags/Neo4jError.php @@ -40,6 +40,7 @@ public static function fromBoltResponse(Response $response): self { /** * @psalm-suppress ImpurePropertyFetch + * * @var array{code: string, message:string} $content */ $content = $response->content; diff --git a/src/Enum/QueryTypeEnum.php b/src/Enum/QueryTypeEnum.php index a505a6f2..01349c51 100644 --- a/src/Enum/QueryTypeEnum.php +++ b/src/Enum/QueryTypeEnum.php @@ -16,6 +16,7 @@ use JsonSerializable; use Laudis\Neo4j\Databags\SummaryCounters; use Laudis\TypedEnum\TypedEnum; +use Stringable; /** * The actual type of query after is has been run. @@ -31,7 +32,7 @@ * * @psalm-suppress MutableDependency */ -final class QueryTypeEnum extends TypedEnum implements JsonSerializable, \Stringable +final class QueryTypeEnum extends TypedEnum implements JsonSerializable, Stringable { private const READ_ONLY = 'read_only'; private const READ_WRITE = 'read_write'; diff --git a/src/Enum/SslMode.php b/src/Enum/SslMode.php index f549db64..6a52131e 100644 --- a/src/Enum/SslMode.php +++ b/src/Enum/SslMode.php @@ -15,6 +15,7 @@ use JsonSerializable; use Laudis\TypedEnum\TypedEnum; +use Stringable; /** * @method static self ENABLE() @@ -28,7 +29,7 @@ * * @psalm-suppress MutableDependency */ -final class SslMode extends TypedEnum implements JsonSerializable, \Stringable +final class SslMode extends TypedEnum implements JsonSerializable, Stringable { private const ENABLE = 'enable'; private const ENABLE_WITH_SELF_SIGNED = 'enable_with_self_signed'; diff --git a/src/Formatter/Specialised/BoltOGMTranslator.php b/src/Formatter/Specialised/BoltOGMTranslator.php index 017b248b..6022048c 100644 --- a/src/Formatter/Specialised/BoltOGMTranslator.php +++ b/src/Formatter/Specialised/BoltOGMTranslator.php @@ -54,6 +54,7 @@ * @psalm-import-type OGMTypes from OGMFormatter * * @psalm-immutable + * * @psalm-pure */ final class BoltOGMTranslator diff --git a/src/Types/Abstract3DPoint.php b/src/Types/Abstract3DPoint.php index 7bef1a42..9d645e70 100644 --- a/src/Types/Abstract3DPoint.php +++ b/src/Types/Abstract3DPoint.php @@ -49,6 +49,7 @@ public function getZ(): float /** * @psalm-suppress ImplementedReturnTypeMismatch + * * @return array{x: float, y: float, z: float, srid: int, crs: Crs} */ public function toArray(): array diff --git a/src/Types/AbstractCypherSequence.php b/src/Types/AbstractCypherSequence.php index c5aeb3e2..2b1e1632 100644 --- a/src/Types/AbstractCypherSequence.php +++ b/src/Types/AbstractCypherSequence.php @@ -13,7 +13,6 @@ namespace Laudis\Neo4j\Types; -use Generator; use function array_key_exists; use function array_reverse; @@ -25,6 +24,7 @@ use function count; use Countable; +use Generator; use function get_object_vars; use function implode; @@ -299,6 +299,7 @@ public function sorted(?callable $comparator = null): self * @return ArrayList * * @psalm-mutation-free + * * @psalm-suppress MixedArrayAccess */ public function pluck(string $key): ArrayList @@ -320,6 +321,7 @@ public function pluck(string $key): ArrayList * @return Map * * @psalm-mutation-free + * * @psalm-suppress MixedArrayAccess */ public function keyBy(string $key): Map diff --git a/tests/Integration/BoltDriverIntegrationTest.php b/tests/Integration/BoltDriverIntegrationTest.php index 73bd724e..f08e50d3 100644 --- a/tests/Integration/BoltDriverIntegrationTest.php +++ b/tests/Integration/BoltDriverIntegrationTest.php @@ -24,6 +24,7 @@ final class BoltDriverIntegrationTest extends EnvironmentAwareIntegrationTest { /** * @throws Exception + * * @psalm-suppress MixedMethodCall */ public function testValidHostname(): void @@ -37,6 +38,7 @@ public function testValidHostname(): void /** * @throws Exception + * * @psalm-suppress MixedMethodCall */ public function testValidUrl(): void diff --git a/tests/Unit/BoltFactoryTest.php b/tests/Unit/BoltFactoryTest.php index 6a24a3e6..20557bbf 100644 --- a/tests/Unit/BoltFactoryTest.php +++ b/tests/Unit/BoltFactoryTest.php @@ -40,7 +40,6 @@ protected function setUp(): void $basicConnectionFactory->method('create') ->willReturn(new Connection($this->createMock(IConnection::class), '')); - $protocolFactory = $this->createMock(ProtocolFactory::class); $protocolFactory->method('createProtocol') ->willReturnCallback(static function (IConnection $connection) { From 1ecbd9ee968d41d007c7bddd691e08480ee920f0 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 18:29:18 +0530 Subject: [PATCH 33/59] update phpunit to latest version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b350f5c2..5ac32876 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,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", From ffdaf5b2e0bcb366ee17e2dd34c020771b43427c Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 19:45:58 +0530 Subject: [PATCH 34/59] fixes phpunit --- .github/workflows/unit-test.yml | 6 ++--- .gitignore | 1 + phpunit.xml.dist | 35 ++++++++++++--------------- src/Bolt/BoltConnection.php | 16 +++++++----- tests/Unit/BoltConnectionPoolTest.php | 7 +++++- tests/Unit/DNSAddressResolverTest.php | 8 +++--- 6 files changed, 40 insertions(+), 33 deletions(-) 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/phpunit.xml.dist b/phpunit.xml.dist index 7da76d3d..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/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index e5ee2f6b..d04443f7 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -284,14 +284,18 @@ public function pull(?int $qid, ?int $fetchSize): array public function __destruct() { - if ($this->protocol()->serverState === ServerState::FAILED && $this->isOpen()) { - if ($this->protocol()->serverState === ServerState::STREAMING || $this->protocol()->serverState === 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; + } + } catch (\Throwable) { - 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; } } 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/DNSAddressResolverTest.php b/tests/Unit/DNSAddressResolverTest.php index ba426412..b5640617 100644 --- a/tests/Unit/DNSAddressResolverTest.php +++ b/tests/Unit/DNSAddressResolverTest.php @@ -28,7 +28,7 @@ protected function setUp(): void public function testResolverGhlenDotCom(): void { - $records = iterator_to_array($this->resolver->getAddresses('test.ghlen.com')); + $records = iterator_to_array($this->resolver->getAddresses('test.ghlen.com'), false); $this->assertEqualsCanonicalizing(['test.ghlen.com', '123.123.123.123', '123.123.123.124'], $records); $this->assertNotEmpty($records); @@ -37,7 +37,7 @@ public function testResolverGhlenDotCom(): void public function testResolverGoogleDotComReverse(): void { - $records = iterator_to_array($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,7 +45,7 @@ public function testResolverGoogleDotComReverse(): void public function testBogus(): void { - $addresses = iterator_to_array($this->resolver->getAddresses('bogus')); - $this->assertEquals('bogus', $addresses); + $addresses = iterator_to_array($this->resolver->getAddresses('bogus'), false); + $this->assertEquals(['bogus'], $addresses); } } From 1f05faec870f69373cdcd73ddd409540c27f116a Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 28 Mar 2024 19:47:04 +0530 Subject: [PATCH 35/59] fix phpunit --- src/Bolt/BoltConnection.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index d04443f7..4a99df99 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -295,7 +295,6 @@ public function __destruct() 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) { - } } From f70d71cf73530691758fc03b827c0c967a70bcde Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Sat, 30 Mar 2024 08:33:46 +0100 Subject: [PATCH 36/59] added more bolt versions --- src/Bolt/BoltConnection.php | 11 +++++------ src/Bolt/Connection.php | 3 --- src/Bolt/ProtocolFactory.php | 11 +++++------ src/Enum/ConnectionProtocol.php | 14 +++++++++++++- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 4a99df99..694802ca 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -16,8 +16,7 @@ use Bolt\enum\ServerState; use Bolt\enum\Signature; use Bolt\protocol\Response; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; +use Bolt\protocol\{V4_4, V5, V5_3, V5_4}; use Laudis\Neo4j\Common\ConnectionConfiguration; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Contracts\ConnectionInterface; @@ -32,7 +31,7 @@ use WeakReference; /** - * @implements ConnectionInterface + * @implements ConnectionInterface * * @psalm-import-type BoltMeta from FormatterInterface */ @@ -54,7 +53,7 @@ class BoltConnection implements ConnectionInterface private array $subscribedResults = []; /** - * @return array{0: V4_4|V5, 1: Connection} + * @return array{0: V4_4|V5|V5_3|V5_4, 1: Connection} */ public function getImplementation(): array { @@ -65,7 +64,7 @@ public function getImplementation(): array * @psalm-mutation-free */ public function __construct( - private V4_4|V5 $boltProtocol, + private V4_4|V5|V5_3|V5_4 $boltProtocol, private readonly Connection $connection, private readonly AuthenticateInterface $auth, private readonly string $userAgent, @@ -254,7 +253,7 @@ public function rollback(): void $this->assertNoFailure($response); } - public function protocol(): V4_4|V5 + public function protocol(): V4_4|V5|V5_3|V5_4 { return $this->boltProtocol; } diff --git a/src/Bolt/Connection.php b/src/Bolt/Connection.php index 196fb1a1..f4171649 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 { - private ?AProtocol $protocol = null; - /** * @param ''|'s'|'ssc' $ssl */ diff --git a/src/Bolt/ProtocolFactory.php b/src/Bolt/ProtocolFactory.php index ebd0f219..8c1fbe22 100644 --- a/src/Bolt/ProtocolFactory.php +++ b/src/Bolt/ProtocolFactory.php @@ -15,25 +15,24 @@ use Bolt\Bolt; use Bolt\connection\IConnection; -use Bolt\protocol\V4_4; -use Bolt\protocol\V5; +use Bolt\protocol\{V4_4, V5, V5_3, V5_4}; use Laudis\Neo4j\Contracts\AuthenticateInterface; use RuntimeException; 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); diff --git a/src/Enum/ConnectionProtocol.php b/src/Enum/ConnectionProtocol.php index ce7cd718..2255ed68 100644 --- a/src/Enum/ConnectionProtocol.php +++ b/src/Enum/ConnectionProtocol.php @@ -20,6 +20,10 @@ 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; @@ -33,6 +37,10 @@ * @method static ConnectionProtocol BOLT_V43() * @method static ConnectionProtocol BOLT_V44() * @method static ConnectionProtocol BOLT_V5() + * @method static ConnectionProtocol BOLT_V5_1() + * @method static ConnectionProtocol BOLT_V5_2() + * @method static ConnectionProtocol BOLT_V5_3() + * @method static ConnectionProtocol BOLT_V5_4() * @method static ConnectionProtocol HTTP() * * @extends TypedEnum @@ -50,6 +58,10 @@ final class ConnectionProtocol extends TypedEnum implements JsonSerializable private const BOLT_V43 = '4.3'; private const BOLT_V44 = '4.4'; private const BOLT_V5 = '5'; + private const BOLT_V5_1 = '5.1'; + private const BOLT_V5_2 = '5.2'; + private const BOLT_V5_3 = '5.3'; + private const BOLT_V5_4 = '5.4'; private const HTTP = 'http'; public function isBolt(): bool @@ -63,7 +75,7 @@ public function isBolt(): bool * * @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()); From 09e13158c14580c3ded757660af5e94175735940 Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Sat, 30 Mar 2024 09:44:07 +0100 Subject: [PATCH 37/59] method has to return hello response, not logon. --- src/Authentication/BasicAuth.php | 10 +++++----- src/Authentication/KerberosAuth.php | 14 +++++++++----- src/Authentication/NoAuth.php | 14 +++++++++----- src/Authentication/OpenIDConnectAuth.php | 14 +++++++++----- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index eff1b5a5..43b006dd 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -57,19 +57,21 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s } /** + * @return array{server: string, connection_id: string, hints: array} * @throws Exception */ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { if (method_exists($protocol, 'logon')) { $protocol->hello(['user_agent' => $userAgent]); - ResponseHelper::getResponse($protocol); - + $response = ResponseHelper::getResponse($protocol); $protocol->logon([ 'scheme' => 'basic', 'principal' => $this->username, 'credentials' => $this->password, ]); + ResponseHelper::getResponse($protocol); + return $response->content; } else { $protocol->hello([ 'user_agent' => $userAgent, @@ -77,10 +79,8 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'principal' => $this->username, 'credentials' => $this->password, ]); + return ResponseHelper::getResponse($protocol)->content; } - - /** @var array{server: string, connection_id: string, hints: list} */ - return ResponseHelper::getResponse($protocol)->content; } public function toString(UriInterface $uri): string diff --git a/src/Authentication/KerberosAuth.php b/src/Authentication/KerberosAuth.php index 849fde27..9fd12e4a 100644 --- a/src/Authentication/KerberosAuth.php +++ b/src/Authentication/KerberosAuth.php @@ -23,6 +23,7 @@ use Laudis\Neo4j\Contracts\AuthenticateInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; +use Exception; use function sprintf; @@ -52,17 +53,22 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s ->withHeader('User-Agent', $userAgent); } + /** + * @return array{server: string, connection_id: string, hints: array} + * @throws Exception + */ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { if (method_exists($protocol, 'logon')) { $protocol->hello(['user_agent' => $userAgent]); - ResponseHelper::getResponse($protocol); - + $response = ResponseHelper::getResponse($protocol); $protocol->logon([ 'scheme' => 'kerberos', 'principal' => '', 'credentials' => $this->token, ]); + ResponseHelper::getResponse($protocol); + return $response->content; } else { $protocol->hello([ 'user_agent' => $userAgent, @@ -70,10 +76,8 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'principal' => '', 'credentials' => $this->token, ]); + return ResponseHelper::getResponse($protocol)->content; } - - /** @var array{server: string, connection_id: string, hints: list} */ - return ResponseHelper::getResponse($protocol); } public function toString(UriInterface $uri): string diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php index 75617fdc..ab8c4496 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -23,6 +23,7 @@ use Laudis\Neo4j\Contracts\AuthenticateInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; +use Exception; use function sprintf; @@ -44,24 +45,27 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s return $request->withHeader('User-Agent', $userAgent); } + /** + * @return array{server: string, connection_id: string, hints: array} + * @throws Exception + */ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { if (method_exists($protocol, 'logon')) { $protocol->hello(['user_agent' => $userAgent]); - ResponseHelper::getResponse($protocol); - + $response = ResponseHelper::getResponse($protocol); $protocol->logon([ 'scheme' => 'none', ]); + ResponseHelper::getResponse($protocol); + return $response->content; } else { $protocol->hello([ 'user_agent' => $userAgent, 'scheme' => 'none', ]); + return ResponseHelper::getResponse($protocol)->content; } - - /** @var array{server: string, connection_id: string, hints: list} */ - return ResponseHelper::getResponse($protocol)->content; } public function toString(UriInterface $uri): string diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php index 61bb7e42..70dde255 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -23,6 +23,7 @@ use Laudis\Neo4j\Contracts\AuthenticateInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; +use Exception; use function sprintf; @@ -49,26 +50,29 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s ->withHeader('User-Agent', $userAgent); } + /** + * @return array{server: string, connection_id: string, hints: array} + * @throws Exception + */ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { if (method_exists($protocol, 'logon')) { $protocol->hello(['user_agent' => $userAgent]); - ResponseHelper::getResponse($protocol); - + $response = ResponseHelper::getResponse($protocol); $protocol->logon([ 'scheme' => 'bearer', 'credentials' => $this->token, ]); + ResponseHelper::getResponse($protocol); + return $response->content; } else { $protocol->hello([ 'user_agent' => $userAgent, 'scheme' => 'bearer', 'credentials' => $this->token, ]); + return ResponseHelper::getResponse($protocol)->content; } - - /** @var array{server: string, connection_id: string, hints: list} */ - return ResponseHelper::getResponse($protocol)->content; } public function toString(UriInterface $uri): string From c4539d006f7865de64fe2c0d6b8045b047460305 Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Sat, 30 Mar 2024 09:51:37 +0100 Subject: [PATCH 38/59] we should do something about the open transaction --- tests/Integration/ClientIntegrationTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Integration/ClientIntegrationTest.php b/tests/Integration/ClientIntegrationTest.php index 08e69107..89eea587 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); } From 1624dc3dbcafb0e0a5bd4c6a3ff6111457d9440b Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Sat, 30 Mar 2024 10:20:51 +0100 Subject: [PATCH 39/59] simplified test --- tests/Integration/ClientIntegrationTest.php | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/Integration/ClientIntegrationTest.php b/tests/Integration/ClientIntegrationTest.php index 89eea587..cd551234 100644 --- a/tests/Integration/ClientIntegrationTest.php +++ b/tests/Integration/ClientIntegrationTest.php @@ -131,14 +131,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 From cf090d3f0049a7e77341625bf86e281afaa2bf5c Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Sat, 30 Mar 2024 19:17:01 +0530 Subject: [PATCH 40/59] static analysis --- src/Authentication/BasicAuth.php | 7 ++++++- src/Authentication/KerberosAuth.php | 9 +++++++-- src/Authentication/NoAuth.php | 9 +++++++-- src/Authentication/OpenIDConnectAuth.php | 9 +++++++-- src/Bolt/BoltConnection.php | 5 ++++- src/Bolt/ProtocolFactory.php | 5 ++++- 6 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index 43b006dd..852060ff 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -57,8 +57,9 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s } /** - * @return array{server: string, connection_id: string, hints: array} * @throws Exception + * + * @return array{server: string, connection_id: string, hints: list} */ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { @@ -71,6 +72,8 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'credentials' => $this->password, ]); ResponseHelper::getResponse($protocol); + + /** @var array{server: string, connection_id: string, hints: list} */ return $response->content; } else { $protocol->hello([ @@ -79,6 +82,8 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'principal' => $this->username, 'credentials' => $this->password, ]); + + /** @var array{server: string, connection_id: string, hints: list} */ return ResponseHelper::getResponse($protocol)->content; } } diff --git a/src/Authentication/KerberosAuth.php b/src/Authentication/KerberosAuth.php index 9fd12e4a..24c2d590 100644 --- a/src/Authentication/KerberosAuth.php +++ b/src/Authentication/KerberosAuth.php @@ -19,11 +19,11 @@ 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 Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; -use Exception; use function sprintf; @@ -54,8 +54,9 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s } /** - * @return array{server: string, connection_id: string, hints: array} * @throws Exception + * + * @return array{server: string, connection_id: string, hints: list} */ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { @@ -68,6 +69,8 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'credentials' => $this->token, ]); ResponseHelper::getResponse($protocol); + + /** @var array{server: string, connection_id: string, hints: list} */ return $response->content; } else { $protocol->hello([ @@ -76,6 +79,8 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'principal' => '', 'credentials' => $this->token, ]); + + /** @var array{server: string, connection_id: string, hints: list} */ return ResponseHelper::getResponse($protocol)->content; } } diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php index ab8c4496..d277a3d5 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -19,11 +19,11 @@ 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 Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; -use Exception; use function sprintf; @@ -46,8 +46,9 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s } /** - * @return array{server: string, connection_id: string, hints: array} * @throws Exception + * + * @return array{server: string, connection_id: string, hints: list} */ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { @@ -58,12 +59,16 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'scheme' => 'none', ]); ResponseHelper::getResponse($protocol); + + /** @var array{server: string, connection_id: string, hints: list} */ return $response->content; } else { $protocol->hello([ 'user_agent' => $userAgent, 'scheme' => 'none', ]); + + /** @var array{server: string, connection_id: string, hints: list} */ return ResponseHelper::getResponse($protocol)->content; } } diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php index 70dde255..bc05d7cc 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -19,11 +19,11 @@ 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 Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; -use Exception; use function sprintf; @@ -51,8 +51,9 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s } /** - * @return array{server: string, connection_id: string, hints: array} * @throws Exception + * + * @return array{server: string, connection_id: string, hints: list} */ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { @@ -64,6 +65,8 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'credentials' => $this->token, ]); ResponseHelper::getResponse($protocol); + + /** @var array{server: string, connection_id: string, hints: list} */ return $response->content; } else { $protocol->hello([ @@ -71,6 +74,8 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $ 'scheme' => 'bearer', 'credentials' => $this->token, ]); + + /** @var array{server: string, connection_id: string, hints: list} */ return ResponseHelper::getResponse($protocol)->content; } } diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 694802ca..2cac0ff5 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -16,7 +16,10 @@ use Bolt\enum\ServerState; use Bolt\enum\Signature; use Bolt\protocol\Response; -use Bolt\protocol\{V4_4, V5, V5_3, V5_4}; +use Bolt\protocol\V4_4; +use Bolt\protocol\V5; +use Bolt\protocol\V5_3; +use Bolt\protocol\V5_4; use Laudis\Neo4j\Common\ConnectionConfiguration; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Contracts\ConnectionInterface; diff --git a/src/Bolt/ProtocolFactory.php b/src/Bolt/ProtocolFactory.php index 8c1fbe22..97335e31 100644 --- a/src/Bolt/ProtocolFactory.php +++ b/src/Bolt/ProtocolFactory.php @@ -15,7 +15,10 @@ use Bolt\Bolt; use Bolt\connection\IConnection; -use Bolt\protocol\{V4_4, V5, V5_3, V5_4}; +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; From e9d6dd817627607283c22f25251f4e3818cac94a Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Sun, 31 Mar 2024 14:21:46 +0200 Subject: [PATCH 41/59] reset, or not to reset --- src/Bolt/BoltUnmanagedTransaction.php | 1 + tests/Integration/ClientIntegrationTest.php | 14 +++++--------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index cf649e68..01bc047b 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -109,6 +109,7 @@ public function runStatement(Statement $statement) $this->config->getAccessMode() ); } catch (Throwable $e) { + $this->connection->reset(); $this->isRolledBack = true; throw $e; } diff --git a/tests/Integration/ClientIntegrationTest.php b/tests/Integration/ClientIntegrationTest.php index cd551234..60b0707f 100644 --- a/tests/Integration/ClientIntegrationTest.php +++ b/tests/Integration/ClientIntegrationTest.php @@ -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 @@ -142,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()); From dc75b6531b37909d133dd89b9a170c677b35dd47 Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Tue, 2 Apr 2024 21:45:54 +0200 Subject: [PATCH 42/59] fixed few tests. moved reset to better place. --- src/Bolt/BoltConnection.php | 1 + src/Bolt/BoltUnmanagedTransaction.php | 11 ++- tests/Integration/ComplexQueryTest.php | 14 ++- .../OGMFormatterIntegrationTest.php | 2 +- .../TransactionIntegrationTest.php | 86 ++++++++++--------- 5 files changed, 64 insertions(+), 50 deletions(-) diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 2cac0ff5..c57d3564 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -353,6 +353,7 @@ public function getUserAgent(): string private function assertNoFailure(Response $response): void { if ($response->signature === Signature::FAILURE) { + $this->protocol()->reset()->getResponse(); //what if the reset fails? what should be expected behaviour? throw Neo4jException::fromBoltResponse($response); } } diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index 01bc047b..519b52dd 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -71,8 +71,14 @@ public function commit(iterable $statements = []): CypherList } }); - $this->connection->commit(); - $this->isCommitted = true; + try { + $this->connection->commit(); + $this->isCommitted = true; + } catch (Throwable $e) { + $this->isCommitted = false; + $this->isRolledBack = true; + throw $e; + } return $tbr; } @@ -109,7 +115,6 @@ public function runStatement(Statement $statement) $this->config->getAccessMode() ); } catch (Throwable $e) { - $this->connection->reset(); $this->isRolledBack = true; throw $e; } 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..69ea3191 100644 --- a/tests/Integration/TransactionIntegrationTest.php +++ b/tests/Integration/TransactionIntegrationTest.php @@ -215,27 +215,28 @@ public function testCommitValidFilledWithInvalidStatement(): void self::assertFalse($tsx->isCommitted()); } - public function testCommitInvalid(): void - { - $tsx = $this->getSession()->beginTransaction(); - $tsx->commit(); - - self::assertTrue($tsx->isFinished()); - 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()); - } + // TODO commit on READY state cause stuck neo4j connection on older version and disconnect at newer +// public function testCommitInvalid(): void +// { +// $tsx = $this->getSession()->beginTransaction(); +// $tsx->commit(); +// +// self::assertTrue($tsx->isFinished()); +// 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::assertTrue($tsx->isRolledBack()); +// self::assertFalse($tsx->isCommitted()); +// } public function testRollbackValid(): void { @@ -247,27 +248,28 @@ public function testRollbackValid(): void self::assertFalse($tsx->isCommitted()); } - public function testRollbackInvalid(): void - { - $tsx = $this->getSession()->beginTransaction(); - $tsx->rollback(); - - self::assertTrue($tsx->isFinished()); - 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()); - } + // TODO rollback on READY state cause stuck neo4j connection on older version and disconnect at newer +// public function testRollbackInvalid(): void +// { +// $tsx = $this->getSession()->beginTransaction(); +// $tsx->rollback(); +// +// self::assertTrue($tsx->isFinished()); +// 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()); +// } // /** // * TODO - rework this test From 8335f8b1b5d9b9b36018a7b598717c3daac89eae Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Wed, 3 Apr 2024 21:54:38 +0200 Subject: [PATCH 43/59] Added server state check for bolt. --- composer.json | 2 +- src/Bolt/BoltConnection.php | 49 +++++++---- src/Bolt/BoltUnmanagedTransaction.php | 10 +-- .../TransactionIntegrationTest.php | 82 ++++++------------- 4 files changed, 64 insertions(+), 79 deletions(-) diff --git a/composer.json b/composer.json index 5ac32876..a1183e37 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "psr/http-factory": "^1.0", "psr/http-client": "^1.0", "php-http/message": "^1.0", - "stefanak-michal/bolt": "^7.0", + "stefanak-michal/bolt": "^7.0.1", "symfony/polyfill-php80": "^1.2", "psr/simple-cache": ">=2.0", "ext-json": "*", diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index c57d3564..76acfb82 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -26,6 +26,7 @@ use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\DatabaseInfo; +use Laudis\Neo4j\Databags\Neo4jError; use Laudis\Neo4j\Enum\AccessMode; use Laudis\Neo4j\Enum\ConnectionProtocol; use Laudis\Neo4j\Exception\Neo4jException; @@ -166,11 +167,10 @@ public function consumeResults(): void */ public function reset(): void { - $response = $this->protocol()->reset() + $response = $this->protocol() + ->reset() ->getResponse(); - $this->assertNoFailure($response); - $this->subscribedResults = []; } @@ -182,12 +182,15 @@ public function reset(): void public function begin(?string $database, ?float $timeout, BookmarkHolder $holder): void { $this->consumeResults(); - $extra = $this->buildRunExtra($database, $timeout, $holder, AccessMode::WRITE()); + if ($this->protocol()->serverState !== ServerState::READY) { + throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'BEGIN\' cannot be handled by a session which isn\'t in the READY state.')]); + } + + $extra = $this->buildRunExtra($database, $timeout, $holder, AccessMode::WRITE()); $response = $this->protocol() ->begin($extra) ->getResponse(); - $this->assertNoFailure($response); } @@ -198,12 +201,14 @@ public function begin(?string $database, ?float $timeout, BookmarkHolder $holder */ public function discard(?int $qid): void { - $extra = $this->buildResultExtra(null, $qid); - $bolt = $this->protocol(); + if (!in_array($this->protocol()->serverState, [ServerState::STREAMING, ServerState::TX_STREAMING], true)) { + throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'DISCARD\' cannot be handled by a session which isn\'t in the STREAMING|TX_STREAMING state.')]); + } - $response = $bolt->discard($extra) + $extra = $this->buildResultExtra(null, $qid); + $response = $this->protocol() + ->discard($extra) ->getResponse(); - $this->assertNoFailure($response); } @@ -216,12 +221,15 @@ public function discard(?int $qid): void */ public function run(string $text, array $parameters, ?string $database, ?float $timeout, BookmarkHolder $holder, ?AccessMode $mode): array { + if (!in_array($this->protocol()->serverState, [ServerState::READY, ServerState::TX_READY, ServerState::TX_STREAMING], true)) { + throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'RUN\' cannot be handled by a session which isn\'t in the READY|TX_READY|TX_STREAMING state.')]); + } + $extra = $this->buildRunExtra($database, $timeout, $holder, $mode); - $response = $this->protocol()->run($text, $parameters, $extra) + $response = $this->protocol() + ->run($text, $parameters, $extra) ->getResponse(); - $this->assertNoFailure($response); - /** @var BoltMeta */ return $response->content; } @@ -234,10 +242,14 @@ public function run(string $text, array $parameters, ?string $database, ?float $ public function commit(): void { $this->consumeResults(); + + if ($this->protocol()->serverState !== ServerState::TX_READY) { + throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'COMMIT\' cannot be handled by a session which isn\'t in the TX_READY state.')]); + } + $response = $this->protocol() ->commit() ->getResponse(); - $this->assertNoFailure($response); } @@ -249,10 +261,14 @@ public function commit(): void public function rollback(): void { $this->consumeResults(); + + if ($this->protocol()->serverState !== ServerState::TX_READY) { + throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'ROLLBACK\' cannot be handled by a session which isn\'t in the TX_READY state.')]); + } + $response = $this->protocol() ->rollback() ->getResponse(); - $this->assertNoFailure($response); } @@ -270,13 +286,16 @@ public function protocol(): V4_4|V5|V5_3|V5_4 */ public function pull(?int $qid, ?int $fetchSize): array { + if (!in_array($this->protocol()->serverState, [ServerState::STREAMING, ServerState::TX_STREAMING], true)) { + throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'PULL\' cannot be handled by a session which isn\'t in the STREAMING|TX_STREAMING state.')]); + } + $extra = $this->buildResultExtra($fetchSize, $qid); $tbr = []; /** @var Response $response */ foreach ($this->protocol()->pull($extra)->getResponses() as $response) { $this->assertNoFailure($response); - $tbr[] = $response->content; } diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index 519b52dd..cf649e68 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -71,14 +71,8 @@ public function commit(iterable $statements = []): CypherList } }); - try { - $this->connection->commit(); - $this->isCommitted = true; - } catch (Throwable $e) { - $this->isCommitted = false; - $this->isRolledBack = true; - throw $e; - } + $this->connection->commit(); + $this->isCommitted = true; return $tbr; } diff --git a/tests/Integration/TransactionIntegrationTest.php b/tests/Integration/TransactionIntegrationTest.php index 69ea3191..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 { @@ -215,28 +211,18 @@ public function testCommitValidFilledWithInvalidStatement(): void self::assertFalse($tsx->isCommitted()); } - // TODO commit on READY state cause stuck neo4j connection on older version and disconnect at newer -// public function testCommitInvalid(): void -// { -// $tsx = $this->getSession()->beginTransaction(); -// $tsx->commit(); -// -// self::assertTrue($tsx->isFinished()); -// 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::assertTrue($tsx->isRolledBack()); -// self::assertFalse($tsx->isCommitted()); -// } + public function testCommitInvalid(): void + { + $tsx = $this->getSession()->beginTransaction(); + $tsx->commit(); + + self::assertTrue($tsx->isFinished()); + self::assertFalse($tsx->isRolledBack()); + self::assertTrue($tsx->isCommitted()); + + $this->expectException(Neo4jException::class); + $tsx->commit(); + } public function testRollbackValid(): void { @@ -248,28 +234,18 @@ public function testRollbackValid(): void self::assertFalse($tsx->isCommitted()); } - // TODO rollback on READY state cause stuck neo4j connection on older version and disconnect at newer -// public function testRollbackInvalid(): void -// { -// $tsx = $this->getSession()->beginTransaction(); -// $tsx->rollback(); -// -// self::assertTrue($tsx->isFinished()); -// 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()); -// } + public function testRollbackInvalid(): void + { + $tsx = $this->getSession()->beginTransaction(); + $tsx->rollback(); + + self::assertTrue($tsx->isFinished()); + self::assertTrue($tsx->isRolledBack()); + self::assertFalse($tsx->isCommitted()); + + $this->expectException(Neo4jException::class); + $tsx->rollback(); + } // /** // * TODO - rework this test @@ -306,9 +282,7 @@ public function testRollbackValid(): void // self::assertCount(3, $cache[$key]); // } - /** - * @doesNotPerformAssertions - */ + #[DoesNotPerformAssertions] public function testTransactionRunNoConsumeResult(): void { $tsx = $this->getSession()->beginTransaction([]); @@ -317,9 +291,7 @@ public function testTransactionRunNoConsumeResult(): void $tsx->commit(); } - /** - * @doesNotPerformAssertions - */ + #[DoesNotPerformAssertions] public function testTransactionRunNoConsumeButSaveResult(): void { $tsx = $this->getSession()->beginTransaction([]); From 2a11d50ceb77bcbb1c8b06b2205c77e51383c26e Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Mon, 8 Apr 2024 04:35:39 +0200 Subject: [PATCH 44/59] raised php versions. removed forced value for phpunit, let it be taken from composer. --- .github/workflows/integration-test-aura.yml | 5 ++--- .github/workflows/integration-test-cluster-neo4j-4.yml | 7 +++---- .github/workflows/integration-test-cluster-neo4j-5.yml | 7 +++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/workflows/integration-test-aura.yml b/.github/workflows/integration-test-aura.yml index 1d7da793..7950983d 100644 --- a/.github/workflows/integration-test-aura.yml +++ b/.github/workflows/integration-test-aura.yml @@ -25,15 +25,14 @@ 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 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..f744958e 100644 --- a/.github/workflows/integration-test-cluster-neo4j-4.yml +++ b/.github/workflows/integration-test-cluster-neo4j-4.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest env: CONNECTION: neo4j://neo4j:testtest@localhost:7688 - name: "Running on PHP 8.0 in a Neo4j 4.4 cluster" + name: "Running on PHP 8.1 in a Neo4j 4.4 cluster" steps: - uses: actions/checkout@v2 @@ -25,14 +25,13 @@ 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 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..20dd4ab8 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,13 @@ 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 testsuite: Integration bootstrap: vendor/autoload.php From 0420bc7f11a445d8d4a0e9a226c05d64f670b51c Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Tue, 9 Apr 2024 12:21:49 +0530 Subject: [PATCH 45/59] use composer version --- .github/workflows/integration-test-aura.yml | 2 +- .github/workflows/integration-test-cluster-neo4j-4.yml | 2 +- .github/workflows/integration-test-cluster-neo4j-5.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration-test-aura.yml b/.github/workflows/integration-test-aura.yml index 1d7da793..8781e3a7 100644 --- a/.github/workflows/integration-test-aura.yml +++ b/.github/workflows/integration-test-aura.yml @@ -34,6 +34,6 @@ jobs: configuration: phpunit.xml.dist php_version: 8.0 memory_limit: 1024M - version: 9 + version: composer 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..95d808d3 100644 --- a/.github/workflows/integration-test-cluster-neo4j-4.yml +++ b/.github/workflows/integration-test-cluster-neo4j-4.yml @@ -32,7 +32,7 @@ jobs: configuration: phpunit.xml.dist php_version: 8.0 memory_limit: 1024M - version: 9 + version: composer 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..3add971a 100644 --- a/.github/workflows/integration-test-cluster-neo4j-5.yml +++ b/.github/workflows/integration-test-cluster-neo4j-5.yml @@ -32,7 +32,7 @@ jobs: configuration: phpunit.xml.dist php_version: 8.0 memory_limit: 1024M - version: 9 + version: composer testsuite: Integration bootstrap: vendor/autoload.php From bf836b28cd141b5214405815f92115552d6d1ef0 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Tue, 9 Apr 2024 13:24:50 +0530 Subject: [PATCH 46/59] use version 10 hardocded --- .github/workflows/integration-test-aura.yml | 2 +- .github/workflows/integration-test-cluster-neo4j-4.yml | 2 +- .github/workflows/integration-test-cluster-neo4j-5.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration-test-aura.yml b/.github/workflows/integration-test-aura.yml index 88ee050f..43dc9954 100644 --- a/.github/workflows/integration-test-aura.yml +++ b/.github/workflows/integration-test-aura.yml @@ -34,6 +34,6 @@ jobs: configuration: phpunit.xml.dist php_version: 8.1 memory_limit: 1024M - version: composer + 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 e1d226e0..c1147c51 100644 --- a/.github/workflows/integration-test-cluster-neo4j-4.yml +++ b/.github/workflows/integration-test-cluster-neo4j-4.yml @@ -32,7 +32,7 @@ jobs: configuration: phpunit.xml.dist php_version: 8.1 memory_limit: 1024M - version: composer + 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 16f8680e..8f97c44c 100644 --- a/.github/workflows/integration-test-cluster-neo4j-5.yml +++ b/.github/workflows/integration-test-cluster-neo4j-5.yml @@ -32,7 +32,7 @@ jobs: configuration: phpunit.xml.dist php_version: 8.1 memory_limit: 1024M - version: composer + version: 10 testsuite: Integration bootstrap: vendor/autoload.php From 4012d620b83b273c48d1de8418cb2207a0e27d07 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Wed, 26 Jun 2024 15:11:26 +0530 Subject: [PATCH 47/59] fix: Unit tests --- Dockerfile | 2 +- tests/Unit/BoltFactoryTest.php | 7 ++++--- tests/Unit/DNSAddressResolverTest.php | 6 +++--- tests/Unit/ParameterHelperTest.php | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) 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/tests/Unit/BoltFactoryTest.php b/tests/Unit/BoltFactoryTest.php index 20557bbf..2d86a660 100644 --- a/tests/Unit/BoltFactoryTest.php +++ b/tests/Unit/BoltFactoryTest.php @@ -14,7 +14,7 @@ namespace Laudis\Neo4j\Tests\Unit; use Bolt\connection\IConnection; -use Bolt\enum\ServerState; +use Bolt\protocol\ServerState; use Bolt\protocol\V5; use Laudis\Neo4j\Authentication\Authenticate; use Laudis\Neo4j\Bolt\BoltConnection; @@ -43,8 +43,9 @@ protected function setUp(): void $protocolFactory = $this->createMock(ProtocolFactory::class); $protocolFactory->method('createProtocol') ->willReturnCallback(static function (IConnection $connection) { - $protocol = new V5(1, $connection); - $protocol->serverState = ServerState::READY; + $serverState = new ServerState(); + $serverState->set(ServerState::READY); + $protocol = new V5(1, $connection, $serverState); return [ $protocol, diff --git a/tests/Unit/DNSAddressResolverTest.php b/tests/Unit/DNSAddressResolverTest.php index b5640617..c109f0c5 100644 --- a/tests/Unit/DNSAddressResolverTest.php +++ b/tests/Unit/DNSAddressResolverTest.php @@ -28,11 +28,11 @@ protected function setUp(): void public function testResolverGhlenDotCom(): void { - $records = iterator_to_array($this->resolver->getAddresses('test.ghlen.com'), false); + $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 diff --git a/tests/Unit/ParameterHelperTest.php b/tests/Unit/ParameterHelperTest.php index 83ce18a0..9d067183 100644 --- a/tests/Unit/ParameterHelperTest.php +++ b/tests/Unit/ParameterHelperTest.php @@ -165,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 From b29a5366d5d36ff717f5fe009e881890f62dc625 Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Sun, 31 Mar 2024 14:21:46 +0200 Subject: [PATCH 48/59] reset, or not to reset --- src/Bolt/BoltUnmanagedTransaction.php | 1 + tests/Integration/ClientIntegrationTest.php | 14 +++++--------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index cf649e68..01bc047b 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -109,6 +109,7 @@ public function runStatement(Statement $statement) $this->config->getAccessMode() ); } catch (Throwable $e) { + $this->connection->reset(); $this->isRolledBack = true; throw $e; } diff --git a/tests/Integration/ClientIntegrationTest.php b/tests/Integration/ClientIntegrationTest.php index cd551234..60b0707f 100644 --- a/tests/Integration/ClientIntegrationTest.php +++ b/tests/Integration/ClientIntegrationTest.php @@ -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 @@ -142,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()); From 0828bbd8cb41feb1e44f135120224b7c825d95c9 Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Tue, 2 Apr 2024 21:45:54 +0200 Subject: [PATCH 49/59] fixed few tests. moved reset to better place. --- src/Bolt/BoltConnection.php | 1 + src/Bolt/BoltUnmanagedTransaction.php | 11 ++- tests/Integration/ComplexQueryTest.php | 14 ++- .../OGMFormatterIntegrationTest.php | 2 +- .../TransactionIntegrationTest.php | 86 ++++++++++--------- 5 files changed, 64 insertions(+), 50 deletions(-) diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 2cac0ff5..c57d3564 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -353,6 +353,7 @@ public function getUserAgent(): string private function assertNoFailure(Response $response): void { if ($response->signature === Signature::FAILURE) { + $this->protocol()->reset()->getResponse(); //what if the reset fails? what should be expected behaviour? throw Neo4jException::fromBoltResponse($response); } } diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index 01bc047b..519b52dd 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -71,8 +71,14 @@ public function commit(iterable $statements = []): CypherList } }); - $this->connection->commit(); - $this->isCommitted = true; + try { + $this->connection->commit(); + $this->isCommitted = true; + } catch (Throwable $e) { + $this->isCommitted = false; + $this->isRolledBack = true; + throw $e; + } return $tbr; } @@ -109,7 +115,6 @@ public function runStatement(Statement $statement) $this->config->getAccessMode() ); } catch (Throwable $e) { - $this->connection->reset(); $this->isRolledBack = true; throw $e; } 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..69ea3191 100644 --- a/tests/Integration/TransactionIntegrationTest.php +++ b/tests/Integration/TransactionIntegrationTest.php @@ -215,27 +215,28 @@ public function testCommitValidFilledWithInvalidStatement(): void self::assertFalse($tsx->isCommitted()); } - public function testCommitInvalid(): void - { - $tsx = $this->getSession()->beginTransaction(); - $tsx->commit(); - - self::assertTrue($tsx->isFinished()); - 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()); - } + // TODO commit on READY state cause stuck neo4j connection on older version and disconnect at newer +// public function testCommitInvalid(): void +// { +// $tsx = $this->getSession()->beginTransaction(); +// $tsx->commit(); +// +// self::assertTrue($tsx->isFinished()); +// 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::assertTrue($tsx->isRolledBack()); +// self::assertFalse($tsx->isCommitted()); +// } public function testRollbackValid(): void { @@ -247,27 +248,28 @@ public function testRollbackValid(): void self::assertFalse($tsx->isCommitted()); } - public function testRollbackInvalid(): void - { - $tsx = $this->getSession()->beginTransaction(); - $tsx->rollback(); - - self::assertTrue($tsx->isFinished()); - 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()); - } + // TODO rollback on READY state cause stuck neo4j connection on older version and disconnect at newer +// public function testRollbackInvalid(): void +// { +// $tsx = $this->getSession()->beginTransaction(); +// $tsx->rollback(); +// +// self::assertTrue($tsx->isFinished()); +// 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()); +// } // /** // * TODO - rework this test From 5ea9bfe613e13062ed8d6226576943ad2672da7b Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Wed, 3 Apr 2024 21:54:38 +0200 Subject: [PATCH 50/59] Added server state check for bolt. --- composer.json | 2 +- src/Bolt/BoltConnection.php | 49 +++++++---- src/Bolt/BoltUnmanagedTransaction.php | 10 +-- .../TransactionIntegrationTest.php | 82 ++++++------------- 4 files changed, 64 insertions(+), 79 deletions(-) diff --git a/composer.json b/composer.json index 5ac32876..a1183e37 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "psr/http-factory": "^1.0", "psr/http-client": "^1.0", "php-http/message": "^1.0", - "stefanak-michal/bolt": "^7.0", + "stefanak-michal/bolt": "^7.0.1", "symfony/polyfill-php80": "^1.2", "psr/simple-cache": ">=2.0", "ext-json": "*", diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index c57d3564..76acfb82 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -26,6 +26,7 @@ use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\DatabaseInfo; +use Laudis\Neo4j\Databags\Neo4jError; use Laudis\Neo4j\Enum\AccessMode; use Laudis\Neo4j\Enum\ConnectionProtocol; use Laudis\Neo4j\Exception\Neo4jException; @@ -166,11 +167,10 @@ public function consumeResults(): void */ public function reset(): void { - $response = $this->protocol()->reset() + $response = $this->protocol() + ->reset() ->getResponse(); - $this->assertNoFailure($response); - $this->subscribedResults = []; } @@ -182,12 +182,15 @@ public function reset(): void public function begin(?string $database, ?float $timeout, BookmarkHolder $holder): void { $this->consumeResults(); - $extra = $this->buildRunExtra($database, $timeout, $holder, AccessMode::WRITE()); + if ($this->protocol()->serverState !== ServerState::READY) { + throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'BEGIN\' cannot be handled by a session which isn\'t in the READY state.')]); + } + + $extra = $this->buildRunExtra($database, $timeout, $holder, AccessMode::WRITE()); $response = $this->protocol() ->begin($extra) ->getResponse(); - $this->assertNoFailure($response); } @@ -198,12 +201,14 @@ public function begin(?string $database, ?float $timeout, BookmarkHolder $holder */ public function discard(?int $qid): void { - $extra = $this->buildResultExtra(null, $qid); - $bolt = $this->protocol(); + if (!in_array($this->protocol()->serverState, [ServerState::STREAMING, ServerState::TX_STREAMING], true)) { + throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'DISCARD\' cannot be handled by a session which isn\'t in the STREAMING|TX_STREAMING state.')]); + } - $response = $bolt->discard($extra) + $extra = $this->buildResultExtra(null, $qid); + $response = $this->protocol() + ->discard($extra) ->getResponse(); - $this->assertNoFailure($response); } @@ -216,12 +221,15 @@ public function discard(?int $qid): void */ public function run(string $text, array $parameters, ?string $database, ?float $timeout, BookmarkHolder $holder, ?AccessMode $mode): array { + if (!in_array($this->protocol()->serverState, [ServerState::READY, ServerState::TX_READY, ServerState::TX_STREAMING], true)) { + throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'RUN\' cannot be handled by a session which isn\'t in the READY|TX_READY|TX_STREAMING state.')]); + } + $extra = $this->buildRunExtra($database, $timeout, $holder, $mode); - $response = $this->protocol()->run($text, $parameters, $extra) + $response = $this->protocol() + ->run($text, $parameters, $extra) ->getResponse(); - $this->assertNoFailure($response); - /** @var BoltMeta */ return $response->content; } @@ -234,10 +242,14 @@ public function run(string $text, array $parameters, ?string $database, ?float $ public function commit(): void { $this->consumeResults(); + + if ($this->protocol()->serverState !== ServerState::TX_READY) { + throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'COMMIT\' cannot be handled by a session which isn\'t in the TX_READY state.')]); + } + $response = $this->protocol() ->commit() ->getResponse(); - $this->assertNoFailure($response); } @@ -249,10 +261,14 @@ public function commit(): void public function rollback(): void { $this->consumeResults(); + + if ($this->protocol()->serverState !== ServerState::TX_READY) { + throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'ROLLBACK\' cannot be handled by a session which isn\'t in the TX_READY state.')]); + } + $response = $this->protocol() ->rollback() ->getResponse(); - $this->assertNoFailure($response); } @@ -270,13 +286,16 @@ public function protocol(): V4_4|V5|V5_3|V5_4 */ public function pull(?int $qid, ?int $fetchSize): array { + if (!in_array($this->protocol()->serverState, [ServerState::STREAMING, ServerState::TX_STREAMING], true)) { + throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'PULL\' cannot be handled by a session which isn\'t in the STREAMING|TX_STREAMING state.')]); + } + $extra = $this->buildResultExtra($fetchSize, $qid); $tbr = []; /** @var Response $response */ foreach ($this->protocol()->pull($extra)->getResponses() as $response) { $this->assertNoFailure($response); - $tbr[] = $response->content; } diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index 519b52dd..cf649e68 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -71,14 +71,8 @@ public function commit(iterable $statements = []): CypherList } }); - try { - $this->connection->commit(); - $this->isCommitted = true; - } catch (Throwable $e) { - $this->isCommitted = false; - $this->isRolledBack = true; - throw $e; - } + $this->connection->commit(); + $this->isCommitted = true; return $tbr; } diff --git a/tests/Integration/TransactionIntegrationTest.php b/tests/Integration/TransactionIntegrationTest.php index 69ea3191..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 { @@ -215,28 +211,18 @@ public function testCommitValidFilledWithInvalidStatement(): void self::assertFalse($tsx->isCommitted()); } - // TODO commit on READY state cause stuck neo4j connection on older version and disconnect at newer -// public function testCommitInvalid(): void -// { -// $tsx = $this->getSession()->beginTransaction(); -// $tsx->commit(); -// -// self::assertTrue($tsx->isFinished()); -// 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::assertTrue($tsx->isRolledBack()); -// self::assertFalse($tsx->isCommitted()); -// } + public function testCommitInvalid(): void + { + $tsx = $this->getSession()->beginTransaction(); + $tsx->commit(); + + self::assertTrue($tsx->isFinished()); + self::assertFalse($tsx->isRolledBack()); + self::assertTrue($tsx->isCommitted()); + + $this->expectException(Neo4jException::class); + $tsx->commit(); + } public function testRollbackValid(): void { @@ -248,28 +234,18 @@ public function testRollbackValid(): void self::assertFalse($tsx->isCommitted()); } - // TODO rollback on READY state cause stuck neo4j connection on older version and disconnect at newer -// public function testRollbackInvalid(): void -// { -// $tsx = $this->getSession()->beginTransaction(); -// $tsx->rollback(); -// -// self::assertTrue($tsx->isFinished()); -// 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()); -// } + public function testRollbackInvalid(): void + { + $tsx = $this->getSession()->beginTransaction(); + $tsx->rollback(); + + self::assertTrue($tsx->isFinished()); + self::assertTrue($tsx->isRolledBack()); + self::assertFalse($tsx->isCommitted()); + + $this->expectException(Neo4jException::class); + $tsx->rollback(); + } // /** // * TODO - rework this test @@ -306,9 +282,7 @@ public function testRollbackValid(): void // self::assertCount(3, $cache[$key]); // } - /** - * @doesNotPerformAssertions - */ + #[DoesNotPerformAssertions] public function testTransactionRunNoConsumeResult(): void { $tsx = $this->getSession()->beginTransaction([]); @@ -317,9 +291,7 @@ public function testTransactionRunNoConsumeResult(): void $tsx->commit(); } - /** - * @doesNotPerformAssertions - */ + #[DoesNotPerformAssertions] public function testTransactionRunNoConsumeButSaveResult(): void { $tsx = $this->getSession()->beginTransaction([]); From 8e26e6cb07b68b7cfb77e3f6a71ec516ccdfdd3e Mon Sep 17 00:00:00 2001 From: Michal Stefanak Date: Mon, 8 Apr 2024 04:35:39 +0200 Subject: [PATCH 51/59] raised php versions. removed forced value for phpunit, let it be taken from composer. --- .github/workflows/integration-test-aura.yml | 5 ++--- .github/workflows/integration-test-cluster-neo4j-4.yml | 7 +++---- .github/workflows/integration-test-cluster-neo4j-5.yml | 7 +++---- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/workflows/integration-test-aura.yml b/.github/workflows/integration-test-aura.yml index 8781e3a7..7950983d 100644 --- a/.github/workflows/integration-test-aura.yml +++ b/.github/workflows/integration-test-aura.yml @@ -25,15 +25,14 @@ 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: composer 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 95d808d3..f744958e 100644 --- a/.github/workflows/integration-test-cluster-neo4j-4.yml +++ b/.github/workflows/integration-test-cluster-neo4j-4.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest env: CONNECTION: neo4j://neo4j:testtest@localhost:7688 - name: "Running on PHP 8.0 in a Neo4j 4.4 cluster" + name: "Running on PHP 8.1 in a Neo4j 4.4 cluster" steps: - uses: actions/checkout@v2 @@ -25,14 +25,13 @@ 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: composer 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 3add971a..20dd4ab8 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,13 @@ 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: composer testsuite: Integration bootstrap: vendor/autoload.php From 2c2e496bcfb5f21c72f0d8f2250d56bb37c65960 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Tue, 9 Apr 2024 13:24:50 +0530 Subject: [PATCH 52/59] use version 10 hardocded --- .github/workflows/integration-test-aura.yml | 1 + .github/workflows/integration-test-cluster-neo4j-4.yml | 1 + .github/workflows/integration-test-cluster-neo4j-5.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/integration-test-aura.yml b/.github/workflows/integration-test-aura.yml index 7950983d..43dc9954 100644 --- a/.github/workflows/integration-test-aura.yml +++ b/.github/workflows/integration-test-aura.yml @@ -34,5 +34,6 @@ jobs: configuration: phpunit.xml.dist php_version: 8.1 memory_limit: 1024M + 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 f744958e..c1147c51 100644 --- a/.github/workflows/integration-test-cluster-neo4j-4.yml +++ b/.github/workflows/integration-test-cluster-neo4j-4.yml @@ -32,6 +32,7 @@ jobs: configuration: phpunit.xml.dist php_version: 8.1 memory_limit: 1024M + 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 20dd4ab8..8f97c44c 100644 --- a/.github/workflows/integration-test-cluster-neo4j-5.yml +++ b/.github/workflows/integration-test-cluster-neo4j-5.yml @@ -32,6 +32,7 @@ jobs: configuration: phpunit.xml.dist php_version: 8.1 memory_limit: 1024M + version: 10 testsuite: Integration bootstrap: vendor/autoload.php From d165f923e02bd2cda275ef2d5e033172dadd2eb5 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Wed, 26 Jun 2024 15:11:26 +0530 Subject: [PATCH 53/59] fix: Unit tests --- Dockerfile | 2 +- tests/Unit/BoltFactoryTest.php | 7 ++++--- tests/Unit/DNSAddressResolverTest.php | 6 +++--- tests/Unit/ParameterHelperTest.php | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) 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/tests/Unit/BoltFactoryTest.php b/tests/Unit/BoltFactoryTest.php index 20557bbf..2d86a660 100644 --- a/tests/Unit/BoltFactoryTest.php +++ b/tests/Unit/BoltFactoryTest.php @@ -14,7 +14,7 @@ namespace Laudis\Neo4j\Tests\Unit; use Bolt\connection\IConnection; -use Bolt\enum\ServerState; +use Bolt\protocol\ServerState; use Bolt\protocol\V5; use Laudis\Neo4j\Authentication\Authenticate; use Laudis\Neo4j\Bolt\BoltConnection; @@ -43,8 +43,9 @@ protected function setUp(): void $protocolFactory = $this->createMock(ProtocolFactory::class); $protocolFactory->method('createProtocol') ->willReturnCallback(static function (IConnection $connection) { - $protocol = new V5(1, $connection); - $protocol->serverState = ServerState::READY; + $serverState = new ServerState(); + $serverState->set(ServerState::READY); + $protocol = new V5(1, $connection, $serverState); return [ $protocol, diff --git a/tests/Unit/DNSAddressResolverTest.php b/tests/Unit/DNSAddressResolverTest.php index b5640617..c109f0c5 100644 --- a/tests/Unit/DNSAddressResolverTest.php +++ b/tests/Unit/DNSAddressResolverTest.php @@ -28,11 +28,11 @@ protected function setUp(): void public function testResolverGhlenDotCom(): void { - $records = iterator_to_array($this->resolver->getAddresses('test.ghlen.com'), false); + $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 diff --git a/tests/Unit/ParameterHelperTest.php b/tests/Unit/ParameterHelperTest.php index 83ce18a0..9d067183 100644 --- a/tests/Unit/ParameterHelperTest.php +++ b/tests/Unit/ParameterHelperTest.php @@ -165,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 From cbee49dc25334daba1e782def49025db5395427c Mon Sep 17 00:00:00 2001 From: exaby73 Date: Wed, 26 Jun 2024 16:00:08 +0530 Subject: [PATCH 54/59] fix: update bolt to newest version and revert changes from previous commit --- src/Neo4j/Neo4jConnectionPool.php | 2 +- tests/Unit/BoltFactoryTest.php | 7 +++---- tests/Unit/ParameterHelperTest.php | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Neo4j/Neo4jConnectionPool.php b/src/Neo4j/Neo4jConnectionPool.php index b416dd01..d98a23c4 100644 --- a/src/Neo4j/Neo4jConnectionPool.php +++ b/src/Neo4j/Neo4jConnectionPool.php @@ -186,7 +186,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() - ->content; + ->getContent(); ['servers' => $servers, 'ttl' => $ttl] = $route['rt']; $ttl += time(); diff --git a/tests/Unit/BoltFactoryTest.php b/tests/Unit/BoltFactoryTest.php index 2d86a660..20557bbf 100644 --- a/tests/Unit/BoltFactoryTest.php +++ b/tests/Unit/BoltFactoryTest.php @@ -14,7 +14,7 @@ namespace Laudis\Neo4j\Tests\Unit; use Bolt\connection\IConnection; -use Bolt\protocol\ServerState; +use Bolt\enum\ServerState; use Bolt\protocol\V5; use Laudis\Neo4j\Authentication\Authenticate; use Laudis\Neo4j\Bolt\BoltConnection; @@ -43,9 +43,8 @@ protected function setUp(): void $protocolFactory = $this->createMock(ProtocolFactory::class); $protocolFactory->method('createProtocol') ->willReturnCallback(static function (IConnection $connection) { - $serverState = new ServerState(); - $serverState->set(ServerState::READY); - $protocol = new V5(1, $connection, $serverState); + $protocol = new V5(1, $connection); + $protocol->serverState = ServerState::READY; return [ $protocol, diff --git a/tests/Unit/ParameterHelperTest.php b/tests/Unit/ParameterHelperTest.php index 9d067183..83ce18a0 100644 --- a/tests/Unit/ParameterHelperTest.php +++ b/tests/Unit/ParameterHelperTest.php @@ -165,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 From 576313110ddd5f07e58b0fd59bcb45ddd49d86ab Mon Sep 17 00:00:00 2001 From: exaby73 Date: Wed, 26 Jun 2024 16:14:35 +0530 Subject: [PATCH 55/59] fix: revert change to getContent --- src/Neo4j/Neo4jConnectionPool.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo4j/Neo4jConnectionPool.php b/src/Neo4j/Neo4jConnectionPool.php index d98a23c4..b416dd01 100644 --- a/src/Neo4j/Neo4jConnectionPool.php +++ b/src/Neo4j/Neo4jConnectionPool.php @@ -186,7 +186,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(); From dfcfaba896a8d2070face277c694e68192070dcc Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Wed, 3 Jul 2024 09:23:50 +0530 Subject: [PATCH 56/59] temp --- src/Authentication/BasicAuth.php | 16 +- src/Authentication/KerberosAuth.php | 11 +- src/Authentication/NoAuth.php | 15 +- src/Authentication/OpenIDConnectAuth.php | 10 +- src/Bolt/BoltDriver.php | 2 +- src/Bolt/BoltUnmanagedTransaction.php | 8 +- src/Bolt/ProtocolFactory.php | 2 +- src/Bolt/SslConfigurationFactory.php | 2 +- src/Contracts/AuthenticateInterface.php | 9 +- src/Contracts/FormatterInterface.php | 116 ---- src/Formatter/BasicFormatter.php | 235 -------- src/Formatter/OGMFormatter.php | 175 ------ src/Formatter/Specialised/HttpMetaInfo.php | 124 ----- .../Specialised/JoltHttpOGMTranslator.php | 464 ---------------- .../Specialised/LegacyHttpOGMTranslator.php | 525 ------------------ src/Formatter/SummarizedResultFormatter.php | 140 ++--- src/Http/HttpConnection.php | 165 ------ src/Http/HttpConnectionPool.php | 130 ----- src/Http/HttpDriver.php | 200 ------- src/Http/HttpHelper.php | 220 -------- src/Http/HttpSession.php | 191 ------- src/Http/HttpUnmanagedTransaction.php | 171 ------ src/Http/RequestFactory.php | 56 -- tests/Unit/Authentication/TestsAuth.php | 2 +- 24 files changed, 53 insertions(+), 2936 deletions(-) delete mode 100644 src/Contracts/FormatterInterface.php delete mode 100644 src/Formatter/BasicFormatter.php delete mode 100644 src/Formatter/OGMFormatter.php delete mode 100644 src/Formatter/Specialised/HttpMetaInfo.php delete mode 100644 src/Formatter/Specialised/JoltHttpOGMTranslator.php delete mode 100644 src/Formatter/Specialised/LegacyHttpOGMTranslator.php delete mode 100644 src/Http/HttpConnection.php delete mode 100644 src/Http/HttpConnectionPool.php delete mode 100644 src/Http/HttpDriver.php delete mode 100644 src/Http/HttpHelper.php delete mode 100644 src/Http/HttpSession.php delete mode 100644 src/Http/HttpUnmanagedTransaction.php delete mode 100644 src/Http/RequestFactory.php diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index d1982983..8fad72f8 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -41,24 +41,10 @@ public function __construct( private string $password ) {} - /** - * @psalm-mutation-free - */ - public function authenticateHttp(RequestInterface $request, 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('Authorization', sprintf('Basic %s:%s', $this->username, $this->password)) - ->withHeader('User-Agent', $userAgent); - } - /** * @throws Exception */ - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array { $response = $bolt->hello(Auth::basic($this->username, $this->password, $userAgent)); if ($response->getSignature() === Response::SIGNATURE_FAILURE) { diff --git a/src/Authentication/KerberosAuth.php b/src/Authentication/KerberosAuth.php index 146b22aa..20067ada 100644 --- a/src/Authentication/KerberosAuth.php +++ b/src/Authentication/KerberosAuth.php @@ -23,7 +23,6 @@ use Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Exception\Neo4jException; use Psr\Http\Message\RequestInterface; -use RuntimeException; use function sprintf; @@ -43,15 +42,7 @@ public function __construct( private string $token ) {} - /** - * @psalm-mutation-free - */ - public function authenticateHttp(RequestInterface $request, string $userAgent): RequestInterface - { - throw new RuntimeException('Cannot authenticate http requests with Kerberos, use bolt instead.'); - } - - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array { $response = $bolt->hello(Auth::kerberos($this->token, $userAgent)); if ($response->getSignature() === Response::SIGNATURE_FAILURE) { diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php index 8239d7c2..15347297 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -32,20 +32,7 @@ */ final class NoAuth implements AuthenticateInterface, Stringable { - /** - * @psalm-mutation-free - */ - public function authenticateHttp(RequestInterface $request, 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|V5_1|V5_2|V5_3 $bolt, string $userAgent): array + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array { $response = $bolt->hello(Auth::none($userAgent)); if ($response->getSignature() === Response::SIGNATURE_FAILURE) { diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php index 66e10fb8..17f66bbb 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -41,15 +41,7 @@ public function __construct( private string $token ) {} - /** - * @psalm-mutation-free - */ - public function authenticateHttp(RequestInterface $request, string $userAgent): RequestInterface - { - throw new RuntimeException('Cannot authenticate http requests with OpenID Connect, use bolt instead.'); - } - - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array { $response = $bolt->hello(Auth::bearer($this->token, $userAgent)); if ($response->getSignature() === Response::SIGNATURE_FAILURE) { diff --git a/src/Bolt/BoltDriver.php b/src/Bolt/BoltDriver.php index 2d4a4682..6452b610 100644 --- a/src/Bolt/BoltDriver.php +++ b/src/Bolt/BoltDriver.php @@ -83,7 +83,7 @@ public function verifyConnectivity(?SessionConfiguration $config = null): bool $config ??= SessionConfiguration::default(); try { GeneratorHelper::getReturnFromGenerator($this->pool->acquire($config)); - } catch (Throwable $e) { + } catch (Throwable) { return false; } diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index bee0a901..8c7318f9 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -33,10 +33,6 @@ /** * Manages a transaction over the bolt protocol. * - * @template T - * - * @implements UnmanagedTransactionInterface - * * @psalm-import-type BoltMeta from FormatterInterface */ final class BoltUnmanagedTransaction implements UnmanagedTransactionInterface @@ -83,7 +79,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)); } @@ -91,7 +87,7 @@ 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()); $start = microtime(true); diff --git a/src/Bolt/ProtocolFactory.php b/src/Bolt/ProtocolFactory.php index ebd0f219..205c51b2 100644 --- a/src/Bolt/ProtocolFactory.php +++ b/src/Bolt/ProtocolFactory.php @@ -36,7 +36,7 @@ public function createProtocol(IConnection $connection, AuthenticateInterface $a 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/SslConfigurationFactory.php b/src/Bolt/SslConfigurationFactory.php index d4f94258..41550619 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}} diff --git a/src/Contracts/AuthenticateInterface.php b/src/Contracts/AuthenticateInterface.php index 4a1ec4b2..67245ba8 100644 --- a/src/Contracts/AuthenticateInterface.php +++ b/src/Contracts/AuthenticateInterface.php @@ -22,19 +22,12 @@ interface AuthenticateInterface { - /** - * @psalm-mutation-free - * - * Authenticates a RequestInterface with the provided configuration Uri and userAgent. - */ - public function authenticateHttp(RequestInterface $request, 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|V5_1|V5_2|V5_3 $bolt, string $userAgent): array; + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array; /** * Returns a string representation of the authentication. 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/Formatter/BasicFormatter.php b/src/Formatter/BasicFormatter.php deleted file mode 100644 index 244e2e5a..00000000 --- a/src/Formatter/BasicFormatter.php +++ /dev/null @@ -1,235 +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 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 0a3fae7d..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 BoltOGMTranslator $boltTranslator, - private JoltHttpOGMTranslator $joltTranslator, - private 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/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 5468de22..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 index d186cfb8..591d90f9 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -13,6 +13,8 @@ namespace Laudis\Neo4j\Formatter; +use Laudis\Neo4j\Databags\Bookmark; +use Laudis\Neo4j\Formatter\Specialised\BoltOGMTranslator; use function in_array; use function is_int; @@ -53,79 +55,62 @@ final class SummarizedResultFormatter implements FormatterInterface { /** + * @psalm-mutation-free + */ + public function __construct( + private BoltOGMTranslator $boltTranslator + ) {} + + /** + * Creates a new instance of itself. + * * @pure */ public static function create(): self { - return new self(OGMFormatter::create()); + return new self(new BoltOGMTranslator()); } /** - * @psalm-mutation-free + * @param BoltMeta $meta + * + * @return CypherList> */ - public function __construct( - private OGMFormatter $formatter - ) {} + public function basicFormat(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; + } /** - * @param CypherList> $results + * @param BoltMeta $meta + * @param list $result * - * @return SummarizedResult> + * @return CypherMap * * @psalm-mutation-free */ - public function formatHttpStats(stdClass $response, HttpConnection $connection, Statement $statement, float $resultAvailableAfter, float $resultConsumedAfter, CypherList $results): SummarizedResult + private function formatRow(array $meta, array $result): CypherMap { - 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'); + /** @var array $map */ + $map = []; + foreach ($meta['fields'] as $i => $column) { + $map[$column] = $this->boltTranslator->mapValueToType($result[$i]); } - /** - * @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); + return new CypherMap($map); } /** @@ -193,7 +178,7 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection ); }); - $formattedResult = $this->formatter->formatBoltResult($meta, $result, $connection, $runStart, $resultAvailableAfter, $statement, $holder); + $formattedResult = $this->basicFormat($meta, $result, $connection, $runStart, $resultAvailableAfter, $statement, $holder); /** * @psalm-suppress MixedArgument @@ -202,45 +187,4 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection */ return (new SummarizedResult($summary, $formattedResult))->withCacheLimit($result->getFetchSize()); } - - /** - * @psalm-mutation-free - * - * @psalm-suppress ImpureMethodCall - */ - public function formatHttpResult(ResponseInterface $response, stdClass $body, HttpConnection $connection, float $resultsAvailableAfter, float $resultsConsumedAfter, iterable $statements): CypherList - { - /** @var list>> */ - $tbr = []; - - $toDecorate = $this->formatter->formatHttpResult($response, $body, $connection, $resultsAvailableAfter, $resultsConsumedAfter, $statements); - $i = 0; - foreach ($statements as $statement) { - /** @var list $results */ - $results = $body->results; - $result = $results[$i]; - $tbr[] = $this->formatHttpStats($result, $connection, $statement, $resultsAvailableAfter, $resultsConsumedAfter, $toDecorate->get($i)); - ++$i; - } - - return new CypherList($tbr); - } - - /** - * @psalm-mutation-free - */ - public function decorateRequest(RequestInterface $request, ConnectionInterface $connection): RequestInterface - { - return $this->formatter->decorateRequest($request, $connection); - } - - /** - * @psalm-mutation-free - */ - public function statementConfigOverride(ConnectionInterface $connection): array - { - return array_merge($this->formatter->statementConfigOverride($connection), [ - 'includeStats' => true, - ]); - } } diff --git a/src/Http/HttpConnection.php b/src/Http/HttpConnection.php deleted file mode 100644 index 84b129b6..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 ClientInterface $client, - /** @psalm-readonly */ - private ConnectionConfiguration $config, - private AuthenticateInterface $authenticate, - private 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 099039a7..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 Resolvable $client, - /** - * @psalm-readonly - */ - private Resolvable $requestFactory, - /** - * @psalm-readonly - */ - private Resolvable $streamFactory, - private AuthenticateInterface $auth, - private string $userAgent, - private 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 5bc00a05..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 string $key; - - /** - * @psalm-mutation-free - * - * @param FormatterInterface $formatter - */ - public function __construct( - private UriInterface $uri, - private DriverConfiguration $config, - private FormatterInterface $formatter, - private 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 4db69e90..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 Resolvable $streamFactory, - /** @psalm-readonly */ - private HttpConnectionPool $pool, - /** @psalm-readonly */ - private SessionConfiguration $config, - /** - * @psalm-readonly - */ - private FormatterInterface $formatter, - /** - * @psalm-readonly - */ - private Resolvable $requestFactory, - /** - * @psalm-readonly - */ - private 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 4ff22c83..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 RequestInterface $request, - /** @psalm-readonly */ - private HttpConnection $connection, - /** @psalm-readonly */ - private StreamFactoryInterface $factory, - /** - * @psalm-readonly - */ - private 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 991b407b..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 RequestFactoryInterface $requestFactory, - /** @readonly */ - private AuthenticateInterface $authenticate, - /** @readonly */ - private UriInterface $authUri, - /** @readonly */ - private 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/tests/Unit/Authentication/TestsAuth.php b/tests/Unit/Authentication/TestsAuth.php index 072dc72f..0ab6cee0 100644 --- a/tests/Unit/Authentication/TestsAuth.php +++ b/tests/Unit/Authentication/TestsAuth.php @@ -79,7 +79,7 @@ public function testAuthenticateBolt( $this->expectExceptionMessage($exception->getMessage()); } - $response = $instance->authenticateBolt($bolt, $userAgent); + $response = $instance->authenticate($bolt, $userAgent); $this->assertEquals($contents, $response); } From 49928c5d43571ecb97d8b48ed89b143b2626661f Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Wed, 3 Jul 2024 09:39:48 +0530 Subject: [PATCH 57/59] test using core1 --- .github/workflows/integration-test-cluster-neo4j-4.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration-test-cluster-neo4j-4.yml b/.github/workflows/integration-test-cluster-neo4j-4.yml index c1147c51..0ad6fcb6 100644 --- a/.github/workflows/integration-test-cluster-neo4j-4.yml +++ b/.github/workflows/integration-test-cluster-neo4j-4.yml @@ -12,7 +12,7 @@ jobs: tests: runs-on: ubuntu-latest env: - CONNECTION: neo4j://neo4j:testtest@localhost:7688 + CONNECTION: neo4j://neo4j:testtest@core1:7688 name: "Running on PHP 8.1 in a Neo4j 4.4 cluster" steps: From 72e1237171adfa7dbe03d7770dccc4e789dcd622 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Wed, 3 Jul 2024 16:55:38 +0530 Subject: [PATCH 58/59] temp commit --- composer.json | 3 +- src/Authentication/BasicAuth.php | 6 +- src/Authentication/KerberosAuth.php | 10 +- src/Authentication/NoAuth.php | 10 +- src/Authentication/OpenIDConnectAuth.php | 29 +- src/Bolt/BoltConnection.php | 307 +++++----- src/Bolt/BoltDriver.php | 8 +- src/Bolt/BoltResult.php | 153 ----- src/Bolt/BoltUnmanagedTransaction.php | 34 +- src/Bolt/ConnectionPool.php | 6 +- src/Bolt/Messages/AbstractMessage.php | 41 ++ src/Bolt/Messages/Begin.php | 14 +- src/Bolt/Messages/Commit.php | 16 +- src/Bolt/Messages/Discard.php | 9 +- src/Bolt/Messages/GoodBye.php | 16 +- src/Bolt/Messages/Hello.php | 6 +- src/Bolt/Messages/Logoff.php | 16 +- src/Bolt/Messages/Logon.php | 7 +- src/Bolt/Messages/Pull.php | 9 +- src/Bolt/Messages/Reset.php | 17 +- src/Bolt/Messages/Rollback.php | 16 +- src/Bolt/Messages/Route.php | 13 +- src/Bolt/Messages/Run.php | 14 +- src/Bolt/Messages/TransactionMessage.php | 30 +- src/Bolt/ProtocolViolationException.php | 7 + src/Bolt/Responses/CommitResponse.php | 12 + src/Bolt/Responses/HelloResponse.php | 20 + src/Bolt/Responses/Record.php | 17 + src/Bolt/Responses/ResultSuccessResponse.php | 36 ++ src/Bolt/Responses/RouteResponse.php | 14 + src/Bolt/Responses/RunResponse.php | 13 + src/Bolt/ResultBatch.php | 17 + src/Bolt/Session.php | 4 +- src/Bolt/SslConfigurationFactory.php | 6 +- src/Bolt/SystemWideConnectionFactory.php | 9 +- src/BoltFactory.php | 3 +- src/Client.php | 1 - src/Common/RoutingTable.php | 16 + src/Common/Server.php | 14 + src/Common/Value.php | 9 + src/Contracts/AddressResolverInterface.php | 26 - src/Contracts/AuthenticateInterface.php | 9 +- src/Contracts/BoltConvertibleInterface.php | 21 - src/Contracts/ClientInterface.php | 132 ---- src/Contracts/ConnectionInterface.php | 96 --- src/Contracts/ConnectionPoolInterface.php | 7 +- src/Contracts/DriverInterface.php | 7 - src/Contracts/HasPropertiesInterface.php | 58 -- src/Contracts/MessageInterface.php | 20 +- src/Contracts/PointInterface.php | 50 -- src/Contracts/SessionInterface.php | 76 --- src/Contracts/TransactionInterface.php | 48 -- .../UnmanagedTransactionInterface.php | 56 -- src/Databags/Bookmark.php | 40 +- src/Databags/BookmarkHolder.php | 15 +- src/Databags/HttpPsrBindings.php | 133 ---- src/Databags/SummarizedResult.php | 5 +- src/Databags/TransactionConfiguration.php | 10 +- src/Enum/AccessMode.php | 24 +- src/Enum/ConnectionProtocol.php | 72 +-- src/Enum/QueryTypeEnum.php | 59 +- src/Enum/RoutingRoles.php | 37 +- src/Enum/SslMode.php | 32 +- src/Enum/TransactionEffect.php | 40 -- src/Exception/NoSuchRecordException.php | 7 + .../Specialised/BoltOGMTranslator.php | 310 ---------- src/Formatter/SummarizedResultFormatter.php | 190 ------ src/Neo4j/Neo4jConnectionPool.php | 9 +- src/Neo4j/Neo4jDriver.php | 2 +- src/Neo4j/RoutingTable.php | 62 -- src/Results/CombinedRecord.php | 68 +++ src/Results/Result.php | 136 +++++ src/TypeCaster.php | 155 ----- src/Types/Abstract3DPoint.php | 63 -- src/Types/AbstractCypherObject.php | 104 ---- src/Types/AbstractCypherSequence.php | 571 ------------------ src/Types/AbstractPoint.php | 78 --- src/Types/AbstractPropertyObject.php | 51 -- src/Types/ArrayList.php | 255 -------- src/Types/Cartesian3DPoint.php | 42 -- src/Types/CartesianPoint.php | 43 -- src/Types/CypherList.php | 120 ---- src/Types/CypherMap.php | 214 ------- src/Types/Date.php | 75 --- src/Types/DateTime.php | 122 ---- src/Types/DateTimeZoneId.php | 111 ---- src/Types/Duration.php | 106 ---- src/Types/LocalDateTime.php | 91 --- src/Types/LocalTime.php | 57 -- src/Types/Map.php | 510 ---------------- src/Types/Node.php | 102 ---- src/Types/Path.php | 82 --- src/Types/Relationship.php | 75 --- src/Types/Time.php | 60 -- src/Types/UnboundRelationship.php | 91 --- src/Types/WGS843DPoint.php | 57 -- src/Types/WGS84Point.php | 58 -- tests/Unit/Bolt/BoltConnectionTest.php | 12 +- 98 files changed, 825 insertions(+), 5325 deletions(-) delete mode 100644 src/Bolt/BoltResult.php create mode 100644 src/Bolt/Messages/AbstractMessage.php create mode 100644 src/Bolt/ProtocolViolationException.php create mode 100644 src/Bolt/Responses/CommitResponse.php create mode 100644 src/Bolt/Responses/HelloResponse.php create mode 100644 src/Bolt/Responses/Record.php create mode 100644 src/Bolt/Responses/ResultSuccessResponse.php create mode 100644 src/Bolt/Responses/RouteResponse.php create mode 100644 src/Bolt/Responses/RunResponse.php create mode 100644 src/Bolt/ResultBatch.php create mode 100644 src/Common/RoutingTable.php create mode 100644 src/Common/Server.php create mode 100644 src/Common/Value.php delete mode 100644 src/Contracts/AddressResolverInterface.php delete mode 100644 src/Contracts/BoltConvertibleInterface.php delete mode 100644 src/Contracts/ClientInterface.php delete mode 100644 src/Contracts/ConnectionInterface.php delete mode 100644 src/Contracts/HasPropertiesInterface.php delete mode 100644 src/Contracts/PointInterface.php delete mode 100644 src/Contracts/SessionInterface.php delete mode 100644 src/Contracts/TransactionInterface.php delete mode 100644 src/Contracts/UnmanagedTransactionInterface.php delete mode 100644 src/Databags/HttpPsrBindings.php delete mode 100644 src/Enum/TransactionEffect.php create mode 100644 src/Exception/NoSuchRecordException.php delete mode 100644 src/Formatter/Specialised/BoltOGMTranslator.php delete mode 100644 src/Formatter/SummarizedResultFormatter.php delete mode 100644 src/Neo4j/RoutingTable.php create mode 100644 src/Results/CombinedRecord.php create mode 100644 src/Results/Result.php delete mode 100644 src/TypeCaster.php delete mode 100644 src/Types/Abstract3DPoint.php delete mode 100644 src/Types/AbstractCypherObject.php delete mode 100644 src/Types/AbstractCypherSequence.php delete mode 100644 src/Types/AbstractPoint.php delete mode 100644 src/Types/AbstractPropertyObject.php delete mode 100644 src/Types/ArrayList.php delete mode 100644 src/Types/Cartesian3DPoint.php delete mode 100644 src/Types/CartesianPoint.php delete mode 100644 src/Types/CypherList.php delete mode 100644 src/Types/CypherMap.php delete mode 100644 src/Types/Date.php delete mode 100644 src/Types/DateTime.php delete mode 100644 src/Types/DateTimeZoneId.php delete mode 100644 src/Types/Duration.php delete mode 100644 src/Types/LocalDateTime.php delete mode 100644 src/Types/LocalTime.php delete mode 100644 src/Types/Map.php delete mode 100644 src/Types/Node.php delete mode 100644 src/Types/Path.php delete mode 100644 src/Types/Relationship.php delete mode 100644 src/Types/Time.php delete mode 100644 src/Types/UnboundRelationship.php delete mode 100644 src/Types/WGS843DPoint.php delete mode 100644 src/Types/WGS84Point.php diff --git a/composer.json b/composer.json index a1183e37..a08adc40 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,8 @@ "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" diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index 9e8c9197..d5dad27e 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -13,10 +13,6 @@ namespace Laudis\Neo4j\Authentication; -use Bolt\helpers\Auth; -use Bolt\protocol\Response; -use function base64_encode; - use Bolt\protocol\V4_4; use Bolt\protocol\V5; use Bolt\protocol\V5_1; @@ -48,7 +44,7 @@ public function __construct( * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticate(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { if (method_exists($protocol, 'logon')) { $protocol->hello(['user_agent' => $userAgent]); diff --git a/src/Authentication/KerberosAuth.php b/src/Authentication/KerberosAuth.php index e2dfdfa1..152586de 100644 --- a/src/Authentication/KerberosAuth.php +++ b/src/Authentication/KerberosAuth.php @@ -18,8 +18,9 @@ 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 Psr\Http\Message\RequestInterface; use function sprintf; @@ -39,20 +40,17 @@ public function __construct( private readonly string $token ) {} - public function authenticate(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { if (method_exists($protocol, 'logon')) { - $protocol->hello(['user_agent' => $userAgent]); - $response = ResponseHelper::getResponse($protocol); $protocol->logon([ 'scheme' => 'kerberos', 'principal' => '', 'credentials' => $this->token, ]); - ResponseHelper::getResponse($protocol); /** @var array{server: string, connection_id: string, hints: list} */ - return $response->content; + return ResponseHelper::getResponse($protocol)->content; } else { $protocol->hello([ 'user_agent' => $userAgent, diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php index 959311b2..983e5806 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -18,8 +18,9 @@ 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 Psr\Http\Message\RequestInterface; use Stringable; /** @@ -29,18 +30,15 @@ */ final class NoAuth implements AuthenticateInterface, Stringable { - public function authenticate(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { if (method_exists($protocol, 'logon')) { - $protocol->hello(['user_agent' => $userAgent]); - $response = ResponseHelper::getResponse($protocol); $protocol->logon([ 'scheme' => 'none', ]); - ResponseHelper::getResponse($protocol); /** @var array{server: string, connection_id: string, hints: list} */ - return $response->content; + return ResponseHelper::getResponse($protocol)->content; } else { $protocol->hello([ 'user_agent' => $userAgent, diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php index 0ea12849..ac5b1374 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -15,6 +15,11 @@ 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 Psr\Http\Message\RequestInterface; @@ -35,37 +40,17 @@ public function __construct( ) {} /** - * @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('Authorization', 'Bearer '.$this->token) - ->withHeader('User-Agent', $userAgent); - } - - /** - * @throws Exception - * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticate(V4_4|V5|V5_1|V5_2|V5_3 $bolt, string $userAgent): array + public function authenticate(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array { if (method_exists($protocol, 'logon')) { - $protocol->hello(['user_agent' => $userAgent]); - $response = ResponseHelper::getResponse($protocol); $protocol->logon([ 'scheme' => 'bearer', 'credentials' => $this->token, ]); - ResponseHelper::getResponse($protocol); - /** @var array{server: string, connection_id: string, hints: list} */ - return $response->content; + return ResponseHelper::getResponse($protocol)->content; } else { $protocol->hello([ 'user_agent' => $userAgent, diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 65c0320d..98c22720 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -22,24 +22,46 @@ 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\Pull; +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\Record; +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\Common\ResponseHelper; +use Laudis\Neo4j\Common\Value; use Laudis\Neo4j\Contracts\ConnectionInterface; +use Laudis\Neo4j\Contracts\MessageInterface; +use Laudis\Neo4j\Databags\Bookmark; +use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\DatabaseInfo; use Laudis\Neo4j\Databags\Neo4jError; 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\Types\CypherList; +use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\UriInterface; +use Throwable; use WeakReference; /** - * @implements ConnectionInterface - * @implements ConnectionInterface - * * @internal + * + * @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 +class BoltConnection { /** * @note We are using references to "subscribed results" to maintain backwards compatibility and try and strike @@ -50,7 +72,7 @@ class BoltConnection implements ConnectionInterface * should introduce a "manual" mode later down the road to allow the end users to optimise the result * consumption themselves. * - * @var list> + * @var list> */ private array $subscribedResults = []; @@ -58,62 +80,18 @@ class BoltConnection implements ConnectionInterface * @psalm-mutation-free */ public function __construct( - private readonly V4_4|V5|V5_1|V5_2|V5_3 $boltProtocol, - /** @psalm-readonly */ + private readonly V4_4|V5|V5_1|V5_2|V5_3 $boltProtocol, private readonly ConnectionConfiguration $config - ) {} - - public function getEncryptionLevel(): string + ) { - return $this->config->getEncryptionLevel(); } /** - * @psalm-mutation-free + * @return ConnectionConfiguration */ - public function getServerAgent(): string + public function getConfig(): ConnectionConfiguration { - 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(); + return $this->config; } /** @@ -124,17 +102,12 @@ public function isOpen(): bool return !in_array($this->protocol()->serverState, [ServerState::DISCONNECTED, ServerState::DEFUNCT], true); } - public function setTimeout(float $timeout): void - { - $this->connection->setTimeout($timeout); - } - public function consumeResults(): void { foreach ($this->subscribedResults as $result) { $result = $result->get(); - if ($result) { - $result->preload(); + if ($result !== null) { + $result->consume(); } } @@ -149,31 +122,44 @@ public function consumeResults(): void */ public function reset(): void { - $response = $this->protocol() - ->reset() - ->getResponse(); - $this->assertNoFailure($response); + $this->sendMessage(new Reset()); $this->subscribedResults = []; } + public function sendMessage(MessageInterface $message): Response + { + $message->send($this->protocol()); + + $response = ResponseHelper::getResponse($this->protocol()); + + $this->assertNoFailure($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(); - if ($this->protocol()->serverState !== ServerState::READY) { - throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'BEGIN\' cannot be handled by a session which isn\'t in the READY state.')]); - } - - $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)); } /** @@ -181,39 +167,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 { - if (!in_array($this->protocol()->serverState, [ServerState::STREAMING, ServerState::TX_STREAMING], true)) { - throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'DISCARD\' cannot be handled by a session which isn\'t in the STREAMING|TX_STREAMING state.')]); - } + $result = $this->sendMessage(new Discard($n, $qid)); - $extra = $this->buildResultExtra(null, $qid); - $response = $this->protocol() - ->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 - { - if (!in_array($this->protocol()->serverState, [ServerState::READY, ServerState::TX_READY, ServerState::TX_STREAMING], true)) { - throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'RUN\' cannot be handled by a session which isn\'t in the READY|TX_READY|TX_STREAMING state.')]); - } - - $extra = $this->buildRunExtra($database, $timeout, $holder, $mode); - $response = $this->protocol() - ->run($text, $parameters, $extra) - ->getResponse(); - $this->assertNoFailure($response); - /** @var BoltMeta */ - return $response->content; + 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'], + ); } /** @@ -221,18 +209,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(); - if ($this->protocol()->serverState !== ServerState::TX_READY) { - throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'COMMIT\' cannot be handled by a session which isn\'t in the TX_READY state.')]); - } + $response = $this->sendMessage(new Commit()); - $response = $this->protocol() - ->commit() - ->getResponse(); - $this->assertNoFailure($response); + return new CommitResponse(array_key_exists('bookmark', $response->content) ? new Bookmark($response->content['bookmark']) : null); } /** @@ -244,17 +227,10 @@ public function rollback(): void { $this->consumeResults(); - if ($this->protocol()->serverState !== ServerState::TX_READY) { - throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'ROLLBACK\' cannot be handled by a session which isn\'t in the TX_READY state.')]); - } - - $response = $this->protocol() - ->rollback() - ->getResponse(); - $this->assertNoFailure($response); + $this->sendMessage(new Rollback()); } - public function protocol(): V4_4|V5|V5_3|V5_4 + public function protocol(): V4_4|V5|V5_1|V5_2|V5_3|V5_4 { return $this->boltProtocol; } @@ -263,26 +239,27 @@ public function protocol(): V4_4|V5|V5_3|V5_4 * 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 + public function pull(int $fetchSize, ?int $qid): ResultBatch { - if (!in_array($this->protocol()->serverState, [ServerState::STREAMING, ServerState::TX_STREAMING], true)) { - throw new Neo4jException([Neo4jError::fromMessageAndCode('Neo.ClientError.Request.Invalid', 'Message \'PULL\' cannot be handled by a session which isn\'t in the STREAMING|TX_STREAMING state.')]); - } + $tbr = []; - $extra = $this->buildResultExtra($fetchSize, $qid); + $message = new Pull($fetchSize, $qid); + $message->send($this->protocol()); - $tbr = []; /** @var Response $response */ - foreach ($this->protocol()->pull($extra)->getResponses() as $response) { + foreach ($this->protocol()->getResponses() as $response) { $this->assertNoFailure($response); - $tbr[] = $response->content; + if ($response->signature === Signature::RECORD) { + $tbr[] = new Record(array_values(array_map(static fn (mixed $value) => new Value($value), $response->content))); + } + + if ($response->signature === Signature::SUCCESS) { + return new ResultBatch($tbr, $this->createResultSuccessResponse($response)); + } } - /** @var non-empty-list */ - return $tbr; + throw new ProtocolViolationException('PULL message must end with a SUCCESS, FAILURE or IGNORED response'); } public function __destruct() @@ -297,48 +274,13 @@ public function __destruct() 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) { + } catch (Throwable) { } } - private function buildRunExtra(?string $database, ?float $timeout, BookmarkHolder $holder, ?AccessMode $mode): array + public function getServerState(): ServerState { - $extra = []; - if ($database !== null) { - $extra['db'] = $database; - } - if ($timeout !== null) { - $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; - } - - 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 - { - return $this->protocol()->serverState->name; + return $this->protocol()->serverState; } public function subscribeResult(CypherList $result): void @@ -346,16 +288,37 @@ public function subscribeResult(CypherList $result): void $this->subscribedResults[] = WeakReference::create($result); } - public function getUserAgent(): string - { - return $this->userAgent; - } - private function assertNoFailure(Response $response): void { if ($response->signature === Signature::FAILURE) { - $this->protocol()->reset()->getResponse(); //what if the reset fails? what should be expected behaviour? + $this->protocol()->reset()->getResponse(); // what if the reset fails? what should be expected behaviour? throw Neo4jException::fromBoltResponse($response); } } + + /** + * @param Response $response + * @return ResultSuccessResponse + */ + private function createResultSuccessResponse(Response $response): ResultSuccessResponse + { + 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, + ); + } + + public function route(): RouteResponse + { + $response = $this->sendMessage(new Route()); + return new RouteResponse(); + } } diff --git a/src/Bolt/BoltDriver.php b/src/Bolt/BoltDriver.php index 6452b610..e6ca3902 100644 --- a/src/Bolt/BoltDriver.php +++ b/src/Bolt/BoltDriver.php @@ -32,8 +32,6 @@ /** * Drives a singular bolt connections. - * - * @psalm-import-type OGMResults from OGMFormatter */ final class BoltDriver implements DriverInterface { @@ -41,9 +39,9 @@ final class BoltDriver implements DriverInterface * @psalm-mutation-free */ public function __construct( - private UriInterface $parsedUrl, - private ConnectionPool $pool, - private SummarizedResultFormatter $formatter + private readonly UriInterface $parsedUrl, + private readonly ConnectionPool $pool, + private readonly SummarizedResultFormatter $formatter ) {} public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null): self 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 8115d064..5bf6618b 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -22,8 +22,8 @@ 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; @@ -32,8 +32,6 @@ /** * Manages a transaction over the bolt protocol. - * - * @psalm-import-type BoltMeta from FormatterInterface */ final class BoltUnmanagedTransaction implements UnmanagedTransactionInterface { @@ -41,33 +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 fn (SummarizedResult $result) => $result->preload()); - $this->connection->commit(); $this->isCommitted = true; - - return $tbr; } public function rollback(): void @@ -89,17 +73,21 @@ public function run(string $statement, iterable $parameters = []): SummarizedRes */ 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/ConnectionPool.php b/src/Bolt/ConnectionPool.php index 82aadb4e..957f4031 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 */ @@ -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 index eab43ae3..265c7933 100644 --- a/src/Bolt/Messages/Begin.php +++ b/src/Bolt/Messages/Begin.php @@ -1,5 +1,16 @@ + * + * 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; @@ -7,10 +18,11 @@ 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 $bolt): void + 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 index 35ad758d..04d78893 100644 --- a/src/Bolt/Messages/Commit.php +++ b/src/Bolt/Messages/Commit.php @@ -1,5 +1,16 @@ + * + * 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; @@ -7,11 +18,12 @@ 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 implements MessageInterface +class Commit extends AbstractMessage implements MessageInterface { - public function send(V4_4|V5|V5_2|V5_1|V5_3 $bolt): void + 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 index bb98f37c..522b4212 100644 --- a/src/Bolt/Messages/Discard.php +++ b/src/Bolt/Messages/Discard.php @@ -18,6 +18,7 @@ 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; /** @@ -25,14 +26,14 @@ * * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-discard */ -class Discard implements MessageInterface +class Discard extends AbstractMessage implements MessageInterface { public function __construct( - private int $n, - private int|null $qid + private readonly int $n, + private readonly int|null $qid ) {} - public function send(V4_4|V5|V5_2|V5_1|V5_3 $bolt): void + public function send(V4_4|V5|V5_2|V5_1|V5_3|V5_4 $bolt): void { $extra = ['n' => $this->n]; if ($this->qid !== null) { diff --git a/src/Bolt/Messages/GoodBye.php b/src/Bolt/Messages/GoodBye.php index 5efc6b1f..e69637dd 100644 --- a/src/Bolt/Messages/GoodBye.php +++ b/src/Bolt/Messages/GoodBye.php @@ -1,5 +1,16 @@ + * + * 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; @@ -7,6 +18,7 @@ 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; /** @@ -14,9 +26,9 @@ * * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-goodbye */ -class GoodBye implements MessageInterface +class GoodBye extends AbstractMessage implements MessageInterface { - public function send(V4_4|V5|V5_2|V5_1|V5_3 $bolt): void + 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 index 23880de0..30d1544a 100644 --- a/src/Bolt/Messages/Hello.php +++ b/src/Bolt/Messages/Hello.php @@ -18,17 +18,17 @@ 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 implements MessageInterface +class Hello extends AbstractMessage implements MessageInterface { /** * @param list $routing @@ -45,7 +45,7 @@ public function __construct( private array $boltAgent, ) {} - public function send(V4_4|V5|V5_2|V5_1|V5_3 $bolt): void + public function send(V4_4|V5|V5_2|V5_1|V5_3|V5_4 $bolt): void { $extra = [ 'auth' => $this->auth, diff --git a/src/Bolt/Messages/Logoff.php b/src/Bolt/Messages/Logoff.php index cb6d97f7..8fb58c8d 100644 --- a/src/Bolt/Messages/Logoff.php +++ b/src/Bolt/Messages/Logoff.php @@ -1,5 +1,16 @@ + * + * 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; @@ -7,12 +18,13 @@ 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 implements MessageInterface +class Logoff extends AbstractMessage implements MessageInterface { - public function send(V4_4|V5|V5_2|V5_1|V5_3 $bolt): void + 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()); diff --git a/src/Bolt/Messages/Logon.php b/src/Bolt/Messages/Logon.php index e53b3356..5f4ec4cb 100644 --- a/src/Bolt/Messages/Logon.php +++ b/src/Bolt/Messages/Logon.php @@ -18,19 +18,20 @@ 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 implements MessageInterface +class Logon extends AbstractMessage implements MessageInterface { /** * @param array{scheme: string}&array $auth */ public function __construct( - private array $auth + private readonly array $auth ) {} - public function send(V4_4|V5|V5_2|V5_1|V5_3 $bolt): void + 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()); diff --git a/src/Bolt/Messages/Pull.php b/src/Bolt/Messages/Pull.php index 52671806..fdd4444c 100644 --- a/src/Bolt/Messages/Pull.php +++ b/src/Bolt/Messages/Pull.php @@ -18,6 +18,7 @@ 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; /** @@ -25,14 +26,14 @@ * * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-pull */ -class Pull implements MessageInterface +class Pull extends AbstractMessage implements MessageInterface { public function __construct( - private int $n, - private int|null $qid + private readonly int $n, + private readonly int|null $qid ) {} - public function send(V4_4|V5|V5_1|V5_2|V5_3 $bolt): void + public function send(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $bolt): void { $extra = ['n' => $this->n]; if ($this->qid !== null) { diff --git a/src/Bolt/Messages/Reset.php b/src/Bolt/Messages/Reset.php index 2a83ce58..b99a71fc 100644 --- a/src/Bolt/Messages/Reset.php +++ b/src/Bolt/Messages/Reset.php @@ -1,5 +1,16 @@ + * + * 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; @@ -7,15 +18,17 @@ 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 implements MessageInterface +class Reset extends AbstractMessage implements MessageInterface { - public function send(V4_4|V5|V5_2|V5_1|V5_3 $bolt): void + 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 index 7aea16c7..a429e30c 100644 --- a/src/Bolt/Messages/Rollback.php +++ b/src/Bolt/Messages/Rollback.php @@ -1,5 +1,16 @@ + * + * 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; @@ -7,11 +18,12 @@ 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 implements MessageInterface +class Rollback extends AbstractMessage implements MessageInterface { - public function send(V4_4|V5|V5_2|V5_1|V5_3 $bolt): void + 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 index 224f97c6..f6c86fa6 100644 --- a/src/Bolt/Messages/Route.php +++ b/src/Bolt/Messages/Route.php @@ -18,6 +18,7 @@ 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; /** @@ -25,16 +26,16 @@ * * @see https://neo4j.com/docs/bolt/current/bolt/message/#messages-route */ -class Route implements MessageInterface +class Route extends AbstractMessage implements MessageInterface { public function __construct( - private array $routing, - private array $bookmarks, - private string|null $database, - private string|null $impersonatedUser, + 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 $bolt): void + public function send(V4_4|V5|V5_2|V5_1|V5_3|V5_4 $bolt): void { $extra = []; diff --git a/src/Bolt/Messages/Run.php b/src/Bolt/Messages/Run.php index c0b331f7..f694954c 100644 --- a/src/Bolt/Messages/Run.php +++ b/src/Bolt/Messages/Run.php @@ -18,8 +18,9 @@ use Bolt\protocol\V5_1; use Bolt\protocol\V5_2; use Bolt\protocol\V5_3; -use Laudis\Neo4j\Contracts\MessageInterface; +use Bolt\protocol\V5_4; use Laudis\Neo4j\Databags\Bookmark; +use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Enum\AccessMode; /** @@ -33,16 +34,15 @@ class Run extends TransactionMessage { /** * @param array $parameters - * @param array $bookmarks * @param array $txMetadata * @param list $notificationsDisabledCategories */ public function __construct( - private string $text, - private array $parameters, - array $bookmarks, + private readonly string $text, + private readonly array $parameters, + BookmarkHolder $bookmarks, int|null $txTimeout, - array $txMetadata, + array|null $txMetadata, AccessMode|null $mode, string|null $database, string|null $impersonatedUser, @@ -52,7 +52,7 @@ public function __construct( parent::__construct($bookmarks, $txTimeout, $txMetadata, $mode, $database, $impersonatedUser, $notificationsMinimumSeverity, $notificationsDisabledCategories); } - public function sendWithPreDecoratedExtraData(array $extra, V4_4|V5|V5_1|V5_2|V5_3 $bolt): void + 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 index 798d21d3..c70eb1f4 100644 --- a/src/Bolt/Messages/TransactionMessage.php +++ b/src/Bolt/Messages/TransactionMessage.php @@ -18,33 +18,34 @@ 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 +abstract class TransactionMessage extends AbstractMessage { /** - * @param array $bookmarks * @param array $txMetadata * @param list $notificationsDisabledCategories */ public function __construct( - private array $bookmarks, - private int|null $txTimeout, - private array $txMetadata, - private AccessMode|null $mode, - private string|null $database, - private string|null $impersonatedUser, - private string|null $notificationsMinimumSeverity, - private array $notificationsDisabledCategories + 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 $bolt): void; + 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 $bolt): void + public function send(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $bolt): void { $extra = $this->basicTransactionExtra(); @@ -54,8 +55,9 @@ public function send(V4_4|V5|V5_1|V5_2|V5_3 $bolt): void private function basicTransactionExtra(): array { $extra = []; - if ($this->bookmarks !== []) { - $extra['bookmarks'] = $this->bookmarks; + $bookmarks = array_map(static fn (Bookmark $bookmark) => $bookmark->bookmark, $this->bookmarks->bookmarks); + if ($bookmarks !== []) { + $extra['bookmarks'] = $bookmarks; } if ($this->mode !== null) { diff --git a/src/Bolt/ProtocolViolationException.php b/src/Bolt/ProtocolViolationException.php new file mode 100644 index 00000000..b529bff5 --- /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/ResultBatch.php b/src/Bolt/ResultBatch.php new file mode 100644 index 00000000..d6ef3c0f --- /dev/null +++ b/src/Bolt/ResultBatch.php @@ -0,0 +1,17 @@ + $records + * @param ResultSuccessResponse $response + */ + public function __construct(public readonly array $records, public readonly ResultSuccessResponse $response) + { + + } +} diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php index e3edab72..1ff8e691 100644 --- a/src/Bolt/Session.php +++ b/src/Bolt/Session.php @@ -29,12 +29,11 @@ use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Neo4j\Neo4jConnectionPool; use Laudis\Neo4j\Types\ArrayList; -use Throwable; /** * A session using bolt connections. */ -final class Session implements SessionInterface +final class Session { /** @psalm-readonly */ private readonly BookmarkHolder $bookmarkHolder; @@ -128,7 +127,6 @@ 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. diff --git a/src/Bolt/SslConfigurationFactory.php b/src/Bolt/SslConfigurationFactory.php index 07ad08a4..44dafb43 100644 --- a/src/Bolt/SslConfigurationFactory.php +++ b/src/Bolt/SslConfigurationFactory.php @@ -33,14 +33,14 @@ 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'; } diff --git a/src/Bolt/SystemWideConnectionFactory.php b/src/Bolt/SystemWideConnectionFactory.php index a3ebfe1b..efa0c261 100644 --- a/src/Bolt/SystemWideConnectionFactory.php +++ b/src/Bolt/SystemWideConnectionFactory.php @@ -13,9 +13,10 @@ namespace Laudis\Neo4j\Bolt; +use function extension_loaded; + use Laudis\Neo4j\Common\UriConfiguration; use Laudis\Neo4j\Contracts\BasicConnectionFactoryInterface; -use function extension_loaded; /** * Singleton connection factory based on the installed extensions. @@ -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 20c80851..879b567a 100644 --- a/src/BoltFactory.php +++ b/src/BoltFactory.php @@ -13,6 +13,8 @@ namespace Laudis\Neo4j; +use function explode; + use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\ProtocolFactory; use Laudis\Neo4j\Bolt\SslConfigurationFactory; @@ -26,7 +28,6 @@ use Laudis\Neo4j\Databags\SessionConfiguration; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Enum\ConnectionProtocol; -use function explode; /** * Small wrapper around the bolt library to easily guarantee only bolt version 3 and up will be created and authenticated. 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/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; - -use Generator; - -interface AddressResolverInterface -{ - /** - * Returns the addresses. - * - * @return Generator - */ - public function getAddresses(string $host): Generator; -} diff --git a/src/Contracts/AuthenticateInterface.php b/src/Contracts/AuthenticateInterface.php index 370ee8c0..dd8c64ee 100644 --- a/src/Contracts/AuthenticateInterface.php +++ b/src/Contracts/AuthenticateInterface.php @@ -18,7 +18,7 @@ use Bolt\protocol\V5_1; use Bolt\protocol\V5_2; use Bolt\protocol\V5_3; -use Psr\Http\Message\RequestInterface; +use Bolt\protocol\V5_4; interface AuthenticateInterface { @@ -27,10 +27,5 @@ interface AuthenticateInterface * * @return array{server: string, connection_id: string, hints: list} */ - public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, 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/BoltConvertibleInterface.php b/src/Contracts/BoltConvertibleInterface.php deleted file mode 100644 index cf60a978..00000000 --- a/src/Contracts/BoltConvertibleInterface.php +++ /dev/null @@ -1,21 +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\protocol\IStructure; - -interface BoltConvertibleInterface -{ - public function convertToBolt(): IStructure; -} 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 efad76a2..00000000 --- a/src/Contracts/ConnectionInterface.php +++ /dev/null @@ -1,96 +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\protocol\ServerState; -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 -{ - /** - * 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. - * - * @return ServerState::* - */ - 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; - - /** - * 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; -} 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 6ca30d17..addd6a2c 100644 --- a/src/Contracts/DriverInterface.php +++ b/src/Contracts/DriverInterface.php @@ -14,20 +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. - * - * @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/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 index 1ada0d4a..113ac901 100644 --- a/src/Contracts/MessageInterface.php +++ b/src/Contracts/MessageInterface.php @@ -1,5 +1,16 @@ + * + * 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; @@ -7,11 +18,16 @@ 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 +interface MessageInterface extends JsonSerializable, Stringable { - public function send(V4_4|V5|V5_1|V5_2|V5_3 $bolt): void; + 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 47f258bc..00000000 --- a/src/Contracts/SessionInterface.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\Contracts; - -use Laudis\Neo4j\Databags\Bookmark; -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Databags\SummarizedResult; -use Laudis\Neo4j\Databags\TransactionConfiguration; -use Laudis\Neo4j\Exception\Neo4jException; -use Laudis\Neo4j\Types\ArrayList; - -/** - * A lightweight container for causally chained sequences of transactions to carry out work. - * - * @extends TransactionInterface - */ -interface SessionInterface extends TransactionInterface -{ - /** - * @param iterable $statements - */ - public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): ArrayList; - - public function runStatement(Statement $statement, ?TransactionConfiguration $config = null): SummarizedResult; - - public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null): SummarizedResult; - - /** - * @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): mixed; - - /** - * @template HandlerResult - * - * @param callable(TransactionInterface):HandlerResult $tsxHandler - * - * @return HandlerResult - */ - public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null): mixed; - - /** - * @template HandlerResult - * - * @param callable(TransactionInterface):HandlerResult $tsxHandler - * - * @return HandlerResult - */ - public function transaction(callable $tsxHandler, ?TransactionConfiguration $config = null): mixed; - - public function getLastBookmark(): Bookmark; -} diff --git a/src/Contracts/TransactionInterface.php b/src/Contracts/TransactionInterface.php deleted file mode 100644 index 6813f453..00000000 --- a/src/Contracts/TransactionInterface.php +++ /dev/null @@ -1,48 +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\SummarizedResult; -use Laudis\Neo4j\Exception\Neo4jException; -use Laudis\Neo4j\Types\ArrayList; - -/** - * Transactions are atomic units of work that may contain one or more query. - * - * @see https://neo4j.com/docs/cypher-manual/current/introduction/transactions/ - */ -interface TransactionInterface -{ - /** - * @param iterable $parameters - * - * @throws Neo4jException - */ - public function run(string $statement, iterable $parameters = []): SummarizedResult; - - /** - * @throws Neo4jException - */ - public function runStatement(Statement $statement): SummarizedResult; - - /** - * @param iterable $statements - * - * @throws Neo4jException - * - * @return ArrayList - */ - public function runStatements(iterable $statements): ArrayList; -} diff --git a/src/Contracts/UnmanagedTransactionInterface.php b/src/Contracts/UnmanagedTransactionInterface.php deleted file mode 100644 index 20f706e0..00000000 --- a/src/Contracts/UnmanagedTransactionInterface.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\Contracts; - -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Types\ArrayList; -use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; - -/** - * An unmanaged transaction needs to be committed or rolled back manually. - * - * @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 ArrayList - */ - public function commit(iterable $statements = []): ArrayList; - - /** - * 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/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/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/TransactionConfiguration.php b/src/Databags/TransactionConfiguration.php index c1dacf0e..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); } 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 2255ed68..26ef763b 100644 --- a/src/Enum/ConnectionProtocol.php +++ b/src/Enum/ConnectionProtocol.php @@ -27,59 +27,33 @@ 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 BOLT_V5_1() - * @method static ConnectionProtocol BOLT_V5_2() - * @method static ConnectionProtocol BOLT_V5_3() - * @method static ConnectionProtocol BOLT_V5_4() - * @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 BOLT_V5_1 = '5.1'; - private const BOLT_V5_2 = '5.2'; - private const BOLT_V5_3 = '5.3'; - private const BOLT_V5_4 = '5.4'; - private const HTTP = 'http'; - public function isBolt(): bool - { - /** @psalm-suppress ImpureMethodCall */ - return $this !== self::HTTP(); - } +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|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 @@ -87,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; } @@ -100,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 01349c51..d5e26f10 100644 --- a/src/Enum/QueryTypeEnum.php +++ b/src/Enum/QueryTypeEnum.php @@ -13,59 +13,10 @@ namespace Laudis\Neo4j\Enum; -use JsonSerializable; -use Laudis\Neo4j\Databags\SummaryCounters; -use Laudis\TypedEnum\TypedEnum; -use Stringable; - -/** - * 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 6a52131e..bba224c1 100644 --- a/src/Enum/SslMode.php +++ b/src/Enum/SslMode.php @@ -17,33 +17,11 @@ 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/NoSuchRecordException.php b/src/Exception/NoSuchRecordException.php new file mode 100644 index 00000000..2a96f595 --- /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\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 - * - * @psalm-pure - */ -final class BoltOGMTranslator -{ - /** - * @var array - */ - private readonly array $rawToTypes; - - public function __construct() - { - /** @psalm-suppress InvalidPropertyAssignmentValue */ - $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_is_list($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/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php deleted file mode 100644 index 591d90f9..00000000 --- a/src/Formatter/SummarizedResultFormatter.php +++ /dev/null @@ -1,190 +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 Laudis\Neo4j\Databags\Bookmark; -use Laudis\Neo4j\Formatter\Specialised\BoltOGMTranslator; -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 -{ - /** - * @psalm-mutation-free - */ - public function __construct( - private BoltOGMTranslator $boltTranslator - ) {} - - /** - * Creates a new instance of itself. - * - * @pure - */ - public static function create(): self - { - return new self(new BoltOGMTranslator()); - } - - /** - * @param BoltMeta $meta - * - * @return CypherList> - */ - public function basicFormat(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; - } - - /** - * @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); - } - - /** - * @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->basicFormat($meta, $result, $connection, $runStart, $resultAvailableAfter, $statement, $holder); - - /** - * @psalm-suppress MixedArgument - * - * @var SummarizedResult> - */ - return (new SummarizedResult($summary, $formattedResult))->withCacheLimit($result->getFetchSize()); - } -} diff --git a/src/Neo4j/Neo4jConnectionPool.php b/src/Neo4j/Neo4jConnectionPool.php index 07ea1306..8a9b6531 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; @@ -31,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; @@ -54,8 +54,7 @@ /** * Connection pool for with auto client-side routing. - * - * @psalm-import-type BasicDriver from DriverInterface + * * @implements ConnectionPoolInterface */ @@ -72,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, 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/Result.php b/src/Results/Result.php new file mode 100644 index 00000000..2aa78e23 --- /dev/null +++ b/src/Results/Result.php @@ -0,0 +1,136 @@ +, CombinedRecord> + */ +final class ResultCursor implements Iterator +{ + private int $position = -1; + private CombinedRecord|ResultSuccessResponse|null $current = null; + + public function __construct( + private readonly BoltConnection $connection, + private readonly Pull $pull, + private readonly RunResponse $response, + ) { + } + + /** + * @return list + */ + public function keys(): array + { + return $this->response->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 { + $response = $this->connection->sendMessage($this->pull); + + if ($response->signature === Signature::RECORD) { + + } + } + + 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->sendMessage($this->pull); + + if ($response->signature === Signature::RECORD) { + + } + } + + 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 9d645e70..00000000 --- a/src/Types/Abstract3DPoint.php +++ /dev/null @@ -1,63 +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; - } - - /** - * @psalm-suppress ImplementedReturnTypeMismatch - * - * @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 2b1e1632..00000000 --- a/src/Types/AbstractCypherSequence.php +++ /dev/null @@ -1,571 +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 Generator; - -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(callable $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 !== null) { - 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 - * - * @psalm-suppress MixedArrayAccess - */ - 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 - * - * @psalm-suppress MixedArrayAccess - */ - 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()) { - /** @var TKey */ - $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->keyCache) !== 0 && 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()) { - /** @var TKey $key */ - $key = $generator->key(); - $this->cache[$key] = $generator->current(); - $this->keyCache[] = $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 ac5da8cf..00000000 --- a/src/Types/AbstractPoint.php +++ /dev/null @@ -1,78 +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 - * - * @extends AbstractPropertyObject - */ -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 b41fe92f..00000000 --- a/src/Types/ArrayList.php +++ /dev/null @@ -1,255 +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 - * - * @psalm-suppress LessSpecificImplementedReturnType - * @psalm-suppress ImplementedReturnTypeMismatch - * - * @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 779e995b..00000000 --- a/src/Types/Map.php +++ /dev/null @@ -1,510 +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 !== null) { - 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 - * - * @psalm-suppress LessSpecificImplementedReturnType - * - * @return self - * - * @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/Unit/Bolt/BoltConnectionTest.php b/tests/Unit/Bolt/BoltConnectionTest.php index 76906976..c54b329e 100644 --- a/tests/Unit/Bolt/BoltConnectionTest.php +++ b/tests/Unit/Bolt/BoltConnectionTest.php @@ -1,10 +1,20 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Laudis\Neo4j\Tests\Unit\Bolt; use PHPUnit\Framework\TestCase; class BoltConnectionTest extends TestCase { - } From 3f193b24b4e7e036c7781f7d6014003e52a1ef9c Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 4 Jul 2024 09:03:35 +0530 Subject: [PATCH 59/59] temp commit --- src/Bolt/BoltConnection.php | 52 +++++-------------- src/Bolt/Connection.php | 2 +- src/Bolt/ConnectionPool.php | 4 +- src/Bolt/Messages/Hello.php | 12 ++--- src/Bolt/ProtocolFactory.php | 2 +- src/Bolt/ProtocolViolationException.php | 2 +- src/Bolt/ResultBatch.php | 17 ------ src/BoltFactory.php | 4 +- src/Common/ResponseHelper.php | 37 ------------- src/Databags/Pair.php | 50 ------------------ .../InvalidCacheArgumentException.php | 2 +- src/Exception/NoSuchRecordException.php | 2 +- src/Neo4j/Neo4jConnectionPool.php | 4 +- src/Results/{Result.php => ResultCursor.php} | 20 +++---- 14 files changed, 42 insertions(+), 168 deletions(-) delete mode 100644 src/Bolt/ResultBatch.php delete mode 100644 src/Common/ResponseHelper.php delete mode 100644 src/Databags/Pair.php rename src/Results/{Result.php => ResultCursor.php} (85%) diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 98c22720..3ad196f0 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -25,33 +25,26 @@ use Laudis\Neo4j\Bolt\Messages\Begin; use Laudis\Neo4j\Bolt\Messages\Commit; use Laudis\Neo4j\Bolt\Messages\Discard; -use Laudis\Neo4j\Bolt\Messages\Pull; 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\Record; 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\Common\ResponseHelper; -use Laudis\Neo4j\Common\Value; use Laudis\Neo4j\Contracts\ConnectionInterface; use Laudis\Neo4j\Contracts\MessageInterface; use Laudis\Neo4j\Databags\Bookmark; use Laudis\Neo4j\Databags\BookmarkHolder; -use Laudis\Neo4j\Databags\DatabaseInfo; -use Laudis\Neo4j\Databags\Neo4jError; 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\ResponseInterface; -use Psr\Http\Message\UriInterface; use Throwable; use WeakReference; @@ -61,7 +54,7 @@ * @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 +final class BoltConnection { /** * @note We are using references to "subscribed results" to maintain backwards compatibility and try and strike @@ -72,7 +65,7 @@ class BoltConnection * should introduce a "manual" mode later down the road to allow the end users to optimise the result * consumption themselves. * - * @var list> + * @var list> */ private array $subscribedResults = []; @@ -130,13 +123,23 @@ public function sendMessage(MessageInterface $message): Response { $message->send($this->protocol()); - $response = ResponseHelper::getResponse($this->protocol()); + $response = $this->getResponse(); $this->assertNoFailure($response); return $response; } + public function getResponse(): Response + { + $response = $this->protocol()->getResponse(); + if ($response->signature === Signature::FAILURE) { + throw Neo4jException::fromBoltResponse($response); + } + + return $response; + } + /** * Begins a transaction. * @@ -235,33 +238,6 @@ 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'. - */ - public function pull(int $fetchSize, ?int $qid): ResultBatch - { - $tbr = []; - - $message = new Pull($fetchSize, $qid); - $message->send($this->protocol()); - - /** @var Response $response */ - foreach ($this->protocol()->getResponses() as $response) { - $this->assertNoFailure($response); - if ($response->signature === Signature::RECORD) { - $tbr[] = new Record(array_values(array_map(static fn (mixed $value) => new Value($value), $response->content))); - } - - if ($response->signature === Signature::SUCCESS) { - return new ResultBatch($tbr, $this->createResultSuccessResponse($response)); - } - } - - throw new ProtocolViolationException('PULL message must end with a SUCCESS, FAILURE or IGNORED response'); - } - public function __destruct() { try { diff --git a/src/Bolt/Connection.php b/src/Bolt/Connection.php index f4171649..c7c0ede9 100644 --- a/src/Bolt/Connection.php +++ b/src/Bolt/Connection.php @@ -15,7 +15,7 @@ use Bolt\connection\IConnection; -class Connection +final class Connection { /** * @param ''|'s'|'ssc' $ssl diff --git a/src/Bolt/ConnectionPool.php b/src/Bolt/ConnectionPool.php index 957f4031..a0cfaff5 100644 --- a/src/Bolt/ConnectionPool.php +++ b/src/Bolt/ConnectionPool.php @@ -90,7 +90,7 @@ public function acquire(SessionConfiguration $config): Generator })(); } - public function release(ConnectionInterface $connection): void + public function release(BoltConnection $connection): void { $this->semaphore->post(); @@ -106,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; diff --git a/src/Bolt/Messages/Hello.php b/src/Bolt/Messages/Hello.php index 30d1544a..b40f18cc 100644 --- a/src/Bolt/Messages/Hello.php +++ b/src/Bolt/Messages/Hello.php @@ -37,12 +37,12 @@ class Hello extends AbstractMessage implements MessageInterface * @param array{product?: string, platform ?: string, language ?: string, language_details ?: string} $boltAgent */ public function __construct( - private array $auth, - private string|null $userAgent, - private array $routing, - private string|null $notificationsMinimumSeverity, - private array $notificationsDisabledCategories, - private array $boltAgent, + 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 diff --git a/src/Bolt/ProtocolFactory.php b/src/Bolt/ProtocolFactory.php index 2b8994b5..ae8896e1 100644 --- a/src/Bolt/ProtocolFactory.php +++ b/src/Bolt/ProtocolFactory.php @@ -22,7 +22,7 @@ use Laudis\Neo4j\Contracts\AuthenticateInterface; use RuntimeException; -class ProtocolFactory +final class ProtocolFactory { /** * @return array{0: V4_4|V5|V5_3|V5_4, 1: array{server: string, connection_id: string, hints: list}} diff --git a/src/Bolt/ProtocolViolationException.php b/src/Bolt/ProtocolViolationException.php index b529bff5..5541314a 100644 --- a/src/Bolt/ProtocolViolationException.php +++ b/src/Bolt/ProtocolViolationException.php @@ -2,6 +2,6 @@ namespace Laudis\Neo4j\Bolt; -class ProtocolViolationException extends \RuntimeException { +final class ProtocolViolationException extends \RuntimeException { } diff --git a/src/Bolt/ResultBatch.php b/src/Bolt/ResultBatch.php deleted file mode 100644 index d6ef3c0f..00000000 --- a/src/Bolt/ResultBatch.php +++ /dev/null @@ -1,17 +0,0 @@ - $records - * @param ResultSuccessResponse $response - */ - public function __construct(public readonly array $records, public readonly ResultSuccessResponse $response) - { - - } -} diff --git a/src/BoltFactory.php b/src/BoltFactory.php index 879b567a..6ff23cf7 100644 --- a/src/BoltFactory.php +++ b/src/BoltFactory.php @@ -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/Common/ResponseHelper.php b/src/Common/ResponseHelper.php deleted file mode 100644 index 9fb6a094..00000000 --- a/src/Common/ResponseHelper.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Common; - -use Bolt\enum\Signature; -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\Exception\Neo4jException; - -class ResponseHelper -{ - public static function getResponse(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol): Response - { - $response = $protocol->getResponse(); - if ($response->signature === Signature::FAILURE) { - throw Neo4jException::fromBoltResponse($response); - } - - return $response; - } -} 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/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 index 2a96f595..0c164c03 100644 --- a/src/Exception/NoSuchRecordException.php +++ b/src/Exception/NoSuchRecordException.php @@ -2,6 +2,6 @@ namespace Laudis\Neo4j\Exception; -class NoSuchRecordException { +final class NoSuchRecordException { } diff --git a/src/Neo4j/Neo4jConnectionPool.php b/src/Neo4j/Neo4jConnectionPool.php index 8a9b6531..1ca0494f 100644 --- a/src/Neo4j/Neo4jConnectionPool.php +++ b/src/Neo4j/Neo4jConnectionPool.php @@ -195,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 diff --git a/src/Results/Result.php b/src/Results/ResultCursor.php similarity index 85% rename from src/Results/Result.php rename to src/Results/ResultCursor.php index 2aa78e23..c7b9ad30 100644 --- a/src/Results/Result.php +++ b/src/Results/ResultCursor.php @@ -19,12 +19,14 @@ final class ResultCursor implements Iterator { private int $position = -1; - private CombinedRecord|ResultSuccessResponse|null $current = null; + private CombinedRecord|null $current = null; + + private ResultSuccessResponse|null $latestResultSuccessResponse = null; public function __construct( private readonly BoltConnection $connection, - private readonly Pull $pull, - private readonly RunResponse $response, + private readonly Pull $pull, + private readonly RunResponse $runResponse, ) { } @@ -33,7 +35,7 @@ public function __construct( */ public function keys(): array { - return $this->response->fields; + return $this->runResponse->fields; } /** @@ -46,11 +48,7 @@ public function keys(): array * @throws NoSuchRecordException if there is not exactly one record left in the stream */ public function single(): CombinedRecord { - $response = $this->connection->sendMessage($this->pull); - - if ($response->signature === Signature::RECORD) { - } } public function only(): null|int|float|array|bool|string|IStructure @@ -112,10 +110,14 @@ public function current(): CombinedRecord public function next(): void { - $response = $this->connection->sendMessage($this->pull); + $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( + ); } }