From ea70b296ea944b6767a4e59e84d89b9173a34e9d Mon Sep 17 00:00:00 2001 From: exaby73 Date: Sun, 29 Dec 2024 21:14:51 +0530 Subject: [PATCH 01/10] feat!: Remove HTTP --- .../integration-test-single-server.yml | 12 - src/Authentication/BasicAuth.php | 17 - src/ClientBuilder.php | 2 +- src/Contracts/AuthenticateInterface.php | 6 - src/Contracts/FormatterInterface.php | 39 -- src/Databags/DriverConfiguration.php | 36 +- src/Databags/HttpPsrBindings.php | 133 ----- src/DriverFactory.php | 28 +- src/Enum/ConnectionProtocol.php | 8 - src/Formatter/OGMFormatter.php | 61 +- src/Formatter/Specialised/HttpMetaInfo.php | 124 ---- .../Specialised/JoltHttpOGMTranslator.php | 466 --------------- .../Specialised/LegacyHttpOGMTranslator.php | 536 ------------------ src/Formatter/SummarizedResultFormatter.php | 109 ---- src/Http/HttpConnection.php | 165 ------ src/Http/HttpConnectionPool.php | 135 ----- src/Http/HttpDriver.php | 206 ------- src/Http/HttpHelper.php | 220 ------- src/Http/HttpSession.php | 191 ------- src/Http/HttpUnmanagedTransaction.php | 171 ------ src/Http/RequestFactory.php | 56 -- src/ParameterHelper.php | 46 +- tests/Integration/ClientIntegrationTest.php | 9 +- 23 files changed, 31 insertions(+), 2745 deletions(-) delete mode 100644 src/Databags/HttpPsrBindings.php delete mode 100644 src/Formatter/Specialised/HttpMetaInfo.php delete mode 100644 src/Formatter/Specialised/JoltHttpOGMTranslator.php delete mode 100644 src/Formatter/Specialised/LegacyHttpOGMTranslator.php delete mode 100644 src/Http/HttpConnection.php delete mode 100644 src/Http/HttpConnectionPool.php delete mode 100644 src/Http/HttpDriver.php delete mode 100644 src/Http/HttpHelper.php delete mode 100644 src/Http/HttpSession.php delete mode 100644 src/Http/HttpUnmanagedTransaction.php delete mode 100644 src/Http/RequestFactory.php diff --git a/.github/workflows/integration-test-single-server.yml b/.github/workflows/integration-test-single-server.yml index bb8e65b3..d0432d1a 100644 --- a/.github/workflows/integration-test-single-server.yml +++ b/.github/workflows/integration-test-single-server.yml @@ -42,12 +42,6 @@ jobs: -e PHP_VERSION=${{ matrix.php }} \ -e CONNECTION=bolt://neo4j:testtest@neo4j \ client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration - - name: Test http:// - run: | - docker compose run \ - -e PHP_VERSION=${{ matrix.php }} \ - -e CONNECTION=http://neo4j:testtest@neo4j \ - client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration tests-v5: runs-on: ubuntu-latest strategy: @@ -81,9 +75,3 @@ jobs: -e PHP_VERSION=${{ matrix.php }} \ -e CONNECTION=bolt://neo4j:testtest@neo4j \ client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration - - name: Test http:// - run: | - docker compose run \ - -e PHP_VERSION=${{ matrix.php }} \ - -e CONNECTION=http://neo4j:testtest@neo4j \ - client ./vendor/bin/phpunit -c phpunit.xml.dist --testsuite Integration diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index 675e667c..7f6fb256 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -13,8 +13,6 @@ namespace Laudis\Neo4j\Authentication; -use function base64_encode; - use Bolt\protocol\V4_4; use Bolt\protocol\V5; use Bolt\protocol\V5_1; @@ -25,7 +23,6 @@ use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Common\ResponseHelper; use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; use Psr\Log\LogLevel; @@ -43,20 +40,6 @@ public function __construct( private readonly ?Neo4jLogger $logger, ) {} - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface - { - $this->logger?->log(LogLevel::DEBUG, 'Authenticating using BasicAuth'); - $combo = base64_encode($this->username.':'.$this->password); - - /** - * @psalm-suppress ImpureMethodCall Request is a pure object: - * - * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message-meta.md#why-value-objects - */ - return $request->withHeader('Authorization', 'Basic '.$combo) - ->withHeader('User-Agent', $userAgent); - } - /** * @throws Exception * diff --git a/src/ClientBuilder.php b/src/ClientBuilder.php index cde9c86e..0ec4b497 100644 --- a/src/ClientBuilder.php +++ b/src/ClientBuilder.php @@ -41,7 +41,7 @@ */ final class ClientBuilder { - public const SUPPORTED_SCHEMES = ['', 'bolt', 'bolt+s', 'bolt+ssc', 'neo4j', 'neo4j+s', 'neo4j+ssc', 'http', 'https']; + public const SUPPORTED_SCHEMES = ['', 'bolt', 'bolt+s', 'bolt+ssc', 'neo4j', 'neo4j+s', 'neo4j+ssc']; /** * @psalm-mutation-free diff --git a/src/Contracts/AuthenticateInterface.php b/src/Contracts/AuthenticateInterface.php index 1379e545..a597d72f 100644 --- a/src/Contracts/AuthenticateInterface.php +++ b/src/Contracts/AuthenticateInterface.php @@ -19,16 +19,10 @@ use Bolt\protocol\V5_2; use Bolt\protocol\V5_3; use Bolt\protocol\V5_4; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; interface AuthenticateInterface { - /** - * Authenticates a RequestInterface with the provided configuration Uri and userAgent. - */ - public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface; - /** * Authenticates a Bolt connection with the provided configuration Uri and userAgent. * diff --git a/src/Contracts/FormatterInterface.php b/src/Contracts/FormatterInterface.php index ded59d2e..d2198dfe 100644 --- a/src/Contracts/FormatterInterface.php +++ b/src/Contracts/FormatterInterface.php @@ -14,16 +14,10 @@ namespace Laudis\Neo4j\Contracts; use Bolt\Bolt; -use JsonException; use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltResult; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Http\HttpConnection; -use Laudis\Neo4j\Types\CypherList; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use stdClass; /** * A formatter (aka Hydrator) is reponsible for formatting the incoming results of the driver. @@ -81,37 +75,4 @@ interface FormatterInterface * @return ResultFormat */ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection $connection, float $runStart, float $resultAvailableAfter, Statement $statement, BookmarkHolder $holder); - - /** - * Formats the results of the HTTP protocol to the unified format. - * - * @param iterable $statements - * - * @throws JsonException - * - * @return CypherList - * - * @psalm-mutation-free - */ - public function formatHttpResult(ResponseInterface $response, stdClass $body, HttpConnection $connection, float $resultsAvailableAfter, float $resultsConsumedAfter, iterable $statements): CypherList; - - /** - * Decorates a request to make make sure it requests the correct format. - * - * @see https://neo4j.com/docs/http-api/current/actions/result-format/ - * - * @psalm-mutation-free - */ - public function decorateRequest(RequestInterface $request, ConnectionInterface $connection): RequestInterface; - - /** - * Overrides the statement config of the HTTP protocol. - * - * @see https://neo4j.com/docs/http-api/current/actions/result-format/ - * - * @return array{resultDataContents?: list<'GRAPH'|'ROW'|'REST'>, includeStats?:bool} - * - * @psalm-mutation-free - */ - public function statementConfigOverride(ConnectionInterface $connection): array; } diff --git a/src/Databags/DriverConfiguration.php b/src/Databags/DriverConfiguration.php index 18d57613..edbd355d 100644 --- a/src/Databags/DriverConfiguration.php +++ b/src/Databags/DriverConfiguration.php @@ -39,8 +39,6 @@ final class DriverConfiguration public const DEFAULT_POOL_SIZE = 0x2F; public const DEFAULT_CACHE_IMPLEMENTATION = Cache::class; public const DEFAULT_ACQUIRE_CONNECTION_TIMEOUT = 2.0; - /** @var callable():(HttpPsrBindings|null)|HttpPsrBindings|null */ - private $httpPsrBindings; /** @var callable():(CacheInterface|null)|CacheInterface|null */ private $cache; /** @var callable():(SemaphoreFactoryInterface|null)|SemaphoreFactoryInterface|null */ @@ -48,7 +46,6 @@ final class DriverConfiguration private ?Neo4jLogger $logger; /** - * @param callable():(HttpPsrBindings|null)|HttpPsrBindings|null $httpPsrBindings * @param callable():(CacheInterface|null)|CacheInterface|null $cache * @param callable():(SemaphoreFactoryInterface|null)|SemaphoreFactoryInterface|null $semaphore * @param string|null $logLevel The log level to use. If null, LogLevel::INFO is used. @@ -57,7 +54,6 @@ final class DriverConfiguration */ public function __construct( private string|null $userAgent, - callable|HttpPsrBindings|null $httpPsrBindings, private SslConfiguration $sslConfig, private int|null $maxPoolSize, CacheInterface|callable|null $cache, @@ -66,7 +62,6 @@ public function __construct( ?string $logLevel, ?LoggerInterface $logger ) { - $this->httpPsrBindings = $httpPsrBindings; $this->cache = $cache; $this->semaphoreFactory = $semaphore; if ($logger !== null) { @@ -77,13 +72,10 @@ public function __construct( } /** - * @param callable():(HttpPsrBindings|null)|HttpPsrBindings|null $httpPsrBindings - * * @pure */ public static function create( ?string $userAgent, - callable|HttpPsrBindings|null $httpPsrBindings, SslConfiguration $sslConfig, int $maxPoolSize, CacheInterface $cache, @@ -94,7 +86,6 @@ public static function create( ): self { return new self( $userAgent, - $httpPsrBindings, $sslConfig, $maxPoolSize, $cache, @@ -107,7 +98,7 @@ public static function create( /** * Creates a default configuration with a user agent based on the driver version - * and HTTP PSR implementation auto-detected from the environment. + * auto-detected from the environment. * * @pure */ @@ -115,7 +106,6 @@ public static function default(): self { return new self( null, - HttpPsrBindings::default(), SslConfiguration::default(), null, null, @@ -160,21 +150,6 @@ public function withUserAgent($userAgent): self return $tbr; } - /** - * Creates a new configuration with the provided bindings. - * - * @param callable():(HttpPsrBindings|null)|HttpPsrBindings|null $bindings - * - * @psalm-immutable - */ - public function withHttpPsrBindings($bindings): self - { - $tbr = clone $this; - $tbr->httpPsrBindings = $bindings; - - return $tbr; - } - /** * @psalm-immutable */ @@ -194,15 +169,6 @@ public function getSslConfiguration(): SslConfiguration return $this->sslConfig; } - public function getHttpPsrBindings(): HttpPsrBindings - { - $this->httpPsrBindings = (is_callable($this->httpPsrBindings)) ? call_user_func( - $this->httpPsrBindings - ) : $this->httpPsrBindings; - - return $this->httpPsrBindings ??= HttpPsrBindings::default(); - } - public function getMaxPoolSize(): int { return $this->maxPoolSize ?? self::DEFAULT_POOL_SIZE; diff --git a/src/Databags/HttpPsrBindings.php b/src/Databags/HttpPsrBindings.php deleted file mode 100644 index 12b0048b..00000000 --- a/src/Databags/HttpPsrBindings.php +++ /dev/null @@ -1,133 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Databags; - -use function call_user_func; - -use Http\Discovery\Psr17FactoryDiscovery; -use Http\Discovery\Psr18ClientDiscovery; - -use function is_callable; - -use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\RequestFactoryInterface; -use Psr\Http\Message\StreamFactoryInterface; - -/** - * Class containing all relevant implementation of the PSR-18 and PSR-17. - * - * @see https://www.php-fig.org/psr/psr-18/ - * @see https://www.php-fig.org/psr/psr-17/ - * @see https://www.php-fig.org/psr/psr-7/ - */ -final class HttpPsrBindings -{ - /** @var ClientInterface|callable():ClientInterface */ - private $client; - /** @var StreamFactoryInterface|callable():StreamFactoryInterface */ - private $streamFactory; - /** @var RequestFactoryInterface|callable():RequestFactoryInterface */ - private $requestFactory; - - /** - * @psalm-mutation-free - * - * @param callable():ClientInterface|ClientInterface|null $client - * @param callable():StreamFactoryInterface|StreamFactoryInterface|null $streamFactory - * @param callable():RequestFactoryInterface|RequestFactoryInterface|null $requestFactory - */ - public function __construct(callable|ClientInterface|null $client = null, callable|StreamFactoryInterface|null $streamFactory = null, callable|RequestFactoryInterface|null $requestFactory = null) - { - $this->client = $client ?? static fn (): ClientInterface => Psr18ClientDiscovery::find(); - $this->streamFactory = $streamFactory ?? static fn (): StreamFactoryInterface => Psr17FactoryDiscovery::findStreamFactory(); - $this->requestFactory = $requestFactory ?? static fn (): RequestFactoryInterface => Psr17FactoryDiscovery::findRequestFactory(); - } - - /** - * @pure - * - * @param callable():ClientInterface|ClientInterface|null $client - * @param callable():StreamFactoryInterface|StreamFactoryInterface|null $streamFactory - * @param callable():RequestFactoryInterface|RequestFactoryInterface|null $requestFactory - */ - public static function create(callable|ClientInterface|null $client = null, callable|StreamFactoryInterface|null $streamFactory = null, callable|RequestFactoryInterface|null $requestFactory = null): self - { - return new self($client, $streamFactory, $requestFactory); - } - - /** - * @pure - */ - public static function default(): self - { - return new self(); - } - - public function getClient(): ClientInterface - { - if (is_callable($this->client)) { - $this->client = call_user_func($this->client); - } - - return $this->client; - } - - /** - * Creates new bindings with the provided client. - * - * @param ClientInterface|callable():ClientInterface $client - */ - public function withClient(ClientInterface|callable $client): self - { - return new self($client, $this->streamFactory, $this->requestFactory); - } - - /** - * Creates new bindings with the provided stream factory. - * - * @param StreamFactoryInterface|callable():StreamFactoryInterface $factory - */ - public function withStreamFactory(StreamFactoryInterface|callable $factory): self - { - return new self($this->client, $factory, $this->requestFactory); - } - - /** - * Creates new bindings with the request factory. - * - * @param RequestFactoryInterface|callable():RequestFactoryInterface $factory - */ - public function withRequestFactory(RequestFactoryInterface|callable $factory): self - { - return new self($this->client, $this->streamFactory, $factory); - } - - public function getStreamFactory(): StreamFactoryInterface - { - if (is_callable($this->streamFactory)) { - $this->streamFactory = call_user_func($this->streamFactory); - } - - return $this->streamFactory; - } - - public function getRequestFactory(): RequestFactoryInterface - { - if (is_callable($this->requestFactory)) { - $this->requestFactory = call_user_func($this->requestFactory); - } - - return $this->requestFactory; - } -} diff --git a/src/DriverFactory.php b/src/DriverFactory.php index 85d7f3b4..aa48c00e 100644 --- a/src/DriverFactory.php +++ b/src/DriverFactory.php @@ -22,8 +22,8 @@ use Laudis\Neo4j\Contracts\DriverInterface; use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\DriverConfiguration; +use Laudis\Neo4j\Exception\UnsupportedScheme; use Laudis\Neo4j\Formatter\OGMFormatter; -use Laudis\Neo4j\Http\HttpDriver; use Laudis\Neo4j\Neo4j\Neo4jDriver; use Psr\Http\Message\UriInterface; @@ -39,6 +39,8 @@ final class DriverFactory * * @param FormatterInterface $formatter * + * @throws UnsupportedScheme + * * @return ( * func_num_args() is 4 * ? DriverInterface @@ -62,7 +64,7 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co return self::createNeo4jDriver($uri, $configuration, $authenticate, $formatter); } - return self::createHttpDriver($uri, $configuration, $authenticate, $formatter); + throw UnsupportedScheme::make($scheme, ['bolt', 'bolt+s', 'bolt+ssc', 'neo4j', 'neo4j+s', 'neo4j+ssc']); } /** @@ -104,26 +106,4 @@ private static function createNeo4jDriver(string|UriInterface $uri, ?DriverConfi return Neo4jDriver::create($uri, $configuration, $authenticate); } - - /** - * @template U - * - * @param FormatterInterface $formatter - * - * @return ( - * func_num_args() is 4 - * ? DriverInterface - * : DriverInterface - * ) - * - * @pure - */ - private static function createHttpDriver(string|UriInterface $uri, ?DriverConfiguration $configuration, ?AuthenticateInterface $authenticate, ?FormatterInterface $formatter = null): DriverInterface - { - if ($formatter !== null) { - return HttpDriver::create($uri, $configuration, $authenticate, $formatter); - } - - return HttpDriver::create($uri, $configuration, $authenticate); - } } diff --git a/src/Enum/ConnectionProtocol.php b/src/Enum/ConnectionProtocol.php index 2255ed68..e5d68b0e 100644 --- a/src/Enum/ConnectionProtocol.php +++ b/src/Enum/ConnectionProtocol.php @@ -41,7 +41,6 @@ * @method static ConnectionProtocol BOLT_V5_2() * @method static ConnectionProtocol BOLT_V5_3() * @method static ConnectionProtocol BOLT_V5_4() - * @method static ConnectionProtocol HTTP() * * @extends TypedEnum * @@ -62,13 +61,6 @@ final class ConnectionProtocol extends TypedEnum implements JsonSerializable private const BOLT_V5_2 = '5.2'; private const BOLT_V5_3 = '5.3'; private const BOLT_V5_4 = '5.4'; - private const HTTP = 'http'; - - public function isBolt(): bool - { - /** @psalm-suppress ImpureMethodCall */ - return $this !== self::HTTP(); - } /** * @pure diff --git a/src/Formatter/OGMFormatter.php b/src/Formatter/OGMFormatter.php index b2a03e08..6de6b90e 100644 --- a/src/Formatter/OGMFormatter.php +++ b/src/Formatter/OGMFormatter.php @@ -17,14 +17,11 @@ use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltResult; -use Laudis\Neo4j\Contracts\ConnectionInterface; use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\Bookmark; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\Statement; use Laudis\Neo4j\Formatter\Specialised\BoltOGMTranslator; -use Laudis\Neo4j\Formatter\Specialised\JoltHttpOGMTranslator; -use Laudis\Neo4j\Formatter\Specialised\LegacyHttpOGMTranslator; use Laudis\Neo4j\Types\Cartesian3DPoint; use Laudis\Neo4j\Types\CartesianPoint; use Laudis\Neo4j\Types\CypherList; @@ -41,11 +38,6 @@ use Laudis\Neo4j\Types\Time; use Laudis\Neo4j\Types\WGS843DPoint; use Laudis\Neo4j\Types\WGS84Point; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use stdClass; - -use function version_compare; /** * Formats the result in a basic OGM (Object Graph Mapping) format by mapping al cypher types to types found in the \Laudis\Neo4j\Types namespace. @@ -68,8 +60,6 @@ final class OGMFormatter implements FormatterInterface */ public function __construct( private readonly BoltOGMTranslator $boltTranslator, - private readonly JoltHttpOGMTranslator $joltTranslator, - private readonly LegacyHttpOGMTranslator $legacyHttpTranslator ) {} /** @@ -79,7 +69,7 @@ public function __construct( */ public static function create(): OGMFormatter { - return new self(new BoltOGMTranslator(), new JoltHttpOGMTranslator(), new LegacyHttpOGMTranslator()); + return new self(new BoltOGMTranslator()); } /** @@ -105,39 +95,6 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection return $tbr; } - /** - * @psalm-mutation-free - */ - public function formatHttpResult( - ResponseInterface $response, - stdClass $body, - ConnectionInterface $connection, - float $resultsAvailableAfter, - float $resultsConsumedAfter, - iterable $statements - ): CypherList { - return $this->decideTranslator($connection)->formatHttpResult( - $response, - $body, - $connection, - $resultsAvailableAfter, - $resultsConsumedAfter, - $statements - ); - } - - /** - * @psalm-mutation-free - */ - private function decideTranslator(ConnectionInterface $connection): LegacyHttpOGMTranslator|JoltHttpOGMTranslator - { - if (version_compare($connection->getServerAgent(), '4.2.5') <= 0) { - return $this->legacyHttpTranslator; - } - - return $this->joltTranslator; - } - /** * @param BoltMeta $meta * @param list $result @@ -156,20 +113,4 @@ private function formatRow(array $meta, array $result): CypherMap return new CypherMap($map); } - - /** - * @psalm-mutation-free - */ - public function decorateRequest(RequestInterface $request, ConnectionInterface $connection): RequestInterface - { - return $this->decideTranslator($connection)->decorateRequest($request); - } - - /** - * @psalm-mutation-free - */ - public function statementConfigOverride(ConnectionInterface $connection): array - { - return $this->decideTranslator($connection)->statementConfigOverride(); - } } diff --git a/src/Formatter/Specialised/HttpMetaInfo.php b/src/Formatter/Specialised/HttpMetaInfo.php deleted file mode 100644 index fb545796..00000000 --- a/src/Formatter/Specialised/HttpMetaInfo.php +++ /dev/null @@ -1,124 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Formatter\Specialised; - -use function is_array; - -use stdClass; - -/** - * @psalm-immutable - */ -final class HttpMetaInfo -{ - /** - * @param list $meta - * @param list $nodes - * @param list $relationships - */ - public function __construct( - private array $meta, - private array $nodes, - private array $relationships, - private int $currentMeta = 0 - ) {} - - /** - * @pure - */ - public static function createFromData(stdClass $data): self - { - /** @var stdClass */ - $graph = $data->graph; - - /** @psalm-suppress MixedArgument */ - return new self($data->meta, $graph->nodes, $graph->relationships); - } - - /** - * @return stdClass|list|null - */ - public function currentMeta() - { - return $this->meta[$this->currentMeta] ?? null; - } - - public function currentNode(): ?stdClass - { - $meta = $this->currentMeta(); - if ($meta === null || is_array($meta)) { - return null; - } - - foreach ($this->nodes as $node) { - if ((int) $node->id === $meta->id) { - return $node; - } - } - - return null; - } - - public function getCurrentRelationship(): ?stdClass - { - $meta = $this->currentMeta(); - if ($meta === null || is_array($meta)) { - return null; - } - - foreach ($this->relationships as $relationship) { - if ((int) $relationship->id === $meta->id) { - return $relationship; - } - } - - return null; - } - - public function getCurrentType(): ?string - { - $currentMeta = $this->currentMeta(); - if (is_array($currentMeta)) { - return 'path'; - } - - if ($currentMeta === null) { - return null; - } - - /** @var string */ - return $currentMeta->type; - } - - public function withNestedMeta(): self - { - $tbr = clone $this; - - $currentMeta = $this->currentMeta(); - if (is_array($currentMeta)) { - $tbr->meta = $currentMeta; - $tbr->currentMeta = 0; - } - - return $tbr; - } - - public function incrementMeta(): self - { - $tbr = clone $this; - ++$tbr->currentMeta; - - return $tbr; - } -} diff --git a/src/Formatter/Specialised/JoltHttpOGMTranslator.php b/src/Formatter/Specialised/JoltHttpOGMTranslator.php deleted file mode 100644 index dadac1ba..00000000 --- a/src/Formatter/Specialised/JoltHttpOGMTranslator.php +++ /dev/null @@ -1,466 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Formatter\Specialised; - -use Closure; - -use const DATE_ATOM; - -use DateInterval; -use DateTimeImmutable; - -use function is_array; - -use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Contracts\PointInterface; -use Laudis\Neo4j\Formatter\OGMFormatter; -use Laudis\Neo4j\Http\HttpHelper; -use Laudis\Neo4j\Types\Cartesian3DPoint; -use Laudis\Neo4j\Types\CartesianPoint; -use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; -use Laudis\Neo4j\Types\Date; -use Laudis\Neo4j\Types\DateTime; -use Laudis\Neo4j\Types\Duration; -use Laudis\Neo4j\Types\LocalDateTime; -use Laudis\Neo4j\Types\LocalTime; -use Laudis\Neo4j\Types\Node; -use Laudis\Neo4j\Types\Path; -use Laudis\Neo4j\Types\Relationship; -use Laudis\Neo4j\Types\Time; -use Laudis\Neo4j\Types\UnboundRelationship; -use Laudis\Neo4j\Types\WGS843DPoint; -use Laudis\Neo4j\Types\WGS84Point; - -use function preg_match; - -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use RuntimeException; -use stdClass; - -use function str_pad; - -use const STR_PAD_RIGHT; - -use function str_replace; -use function str_starts_with; -use function strtolower; - -use UnexpectedValueException; - -/** - * @psalm-immutable - * - * @psalm-import-type OGMTypes from OGMFormatter - * - * @psalm-suppress PossiblyUndefinedArrayOffset - */ -final class JoltHttpOGMTranslator -{ - /** @var array */ - private array $rawToTypes; - - public function __construct() - { - /** @psalm-suppress InvalidPropertyAssignmentValue */ - $this->rawToTypes = [ - '?' => static fn (string $value): bool => strtolower($value) === 'true', - 'Z' => static fn (string $value): int => (int) $value, - 'R' => static fn (string $value): float => (float) $value, - 'U' => static fn (string $value): string => $value, - 'T' => Closure::fromCallable($this->translateDateTime(...)), - '@' => Closure::fromCallable($this->translatePoint(...)), - '#' => Closure::fromCallable($this->translateBinary(...)), - '[]' => Closure::fromCallable($this->translateList(...)), - '{}' => Closure::fromCallable($this->translateMap(...)), - '()' => Closure::fromCallable($this->translateNode(...)), - '->' => Closure::fromCallable($this->translateRightRelationship(...)), - '<-' => Closure::fromCallable($this->translateLeftRelationship(...)), - '..' => Closure::fromCallable($this->translatePath(...)), - ]; - } - - public function decorateRequest(RequestInterface $request): RequestInterface - { - /** @psalm-suppress ImpureMethodCall */ - return $request->withHeader( - 'Accept', - 'application/vnd.neo4j.jolt+json-seq;strict=true;charset=UTF-8' - ); - } - - /** - * @return array{resultDataContents?: list<'GRAPH'|'ROW'|'REST'>, includeStats?:bool} - */ - public function statementConfigOverride(): array - { - return []; - } - - /** - * @return CypherList>> - */ - public function formatHttpResult( - ResponseInterface $response, - stdClass $body, - ConnectionInterface $connection, - float $resultsAvailableAfter, - float $resultsConsumedAfter, - iterable $statements - ): CypherList { - $allResults = []; - /** @var stdClass $result */ - foreach ($body->results as $result) { - /** @var stdClass $header */ - $header = $result->header; - /** @var list $fields */ - $fields = $header->fields; - $rows = []; - - /** @var list $data */ - foreach ($result->data as $data) { - $row = []; - foreach ($data as $key => $value) { - $row[$fields[$key]] = $this->translateJoltType($value); - } - $rows[] = new CypherMap($row); - } - $allResults[] = new CypherList($rows); - } - - return new CypherList($allResults); - } - - /** - * @return OGMTypes - */ - private function translateJoltType(?stdClass $value) - { - if (is_null($value)) { - return null; - } - - /** @var mixed $input */ - [$key, $input] = HttpHelper::splitJoltSingleton($value); - if (!array_key_exists($key, $this->rawToTypes)) { - throw new UnexpectedValueException('Unexpected Jolt key: '.$key); - } - - return $this->rawToTypes[$key]($input); - } - - /** - * Assumes that 2D points are of the form "SRID=$srid;POINT($x $y)" and 3D points are of the form "SRID=$srid;POINT Z($x $y $z)". - * - * @throws UnexpectedValueException - */ - private function translatePoint(string $value): PointInterface - { - [$srid, $coordinates] = explode(';', $value, 2); - - $srid = $this->getSRID($srid); - $coordinates = $this->getCoordinates($coordinates); - - if ($srid === CartesianPoint::SRID) { - return new CartesianPoint( - (float) $coordinates[0], - (float) $coordinates[1], - ); - } - if ($srid === Cartesian3DPoint::SRID) { - return new Cartesian3DPoint( - (float) $coordinates[0], - (float) $coordinates[1], - (float) ($coordinates[2] ?? 0.0), - ); - } - if ($srid === WGS84Point::SRID) { - return new WGS84Point( - (float) $coordinates[0], - (float) $coordinates[1], - ); - } - if ($srid === WGS843DPoint::SRID) { - return new WGS843DPoint( - (float) $coordinates[0], - (float) $coordinates[1], - (float) ($coordinates[2] ?? 0.0), - ); - } - throw new UnexpectedValueException('A point with srid '.$srid.' has been returned, which has not been implemented.'); - } - - private function getSRID(string $value): int - { - $matches = []; - if (!preg_match('/^SRID=([0-9]+)$/', $value, $matches)) { - throw new UnexpectedValueException('Unexpected SRID string: '.$value); - } - - /** @var array{0: string, 1: string} $matches */ - return (int) $matches[1]; - } - - /** - * @return array{0: string, 1: string, 2?: string} $coordinates - */ - private function getCoordinates(string $value): array - { - $matches = []; - if (!preg_match('/^POINT ?(Z?) ?\(([0-9. ]+)\)$/', $value, $matches)) { - throw new UnexpectedValueException('Unexpected point coordinates string: '.$value); - } - /** @var array{0: string, 1: string, 2: string} $matches */ - $coordinates = explode(' ', $matches[2]); - if ($matches[1] === 'Z' && count($coordinates) !== 3) { - throw new UnexpectedValueException('Expected 3 coordinates in string: '.$value); - } - - if ($matches[1] !== 'Z' && count($coordinates) !== 2) { - throw new UnexpectedValueException('Expected 2 coordinates in string: '.$value); - } - - /** @var array{0: string, 1: string, 2?: string} */ - return $coordinates; - } - - /** - * @return CypherMap - */ - private function translateMap(stdClass $value): CypherMap - { - return new CypherMap( - function () use ($value) { - /** @var stdClass|array|null $element */ - foreach ((array) $value as $key => $element) { - // There is an odd case in the JOLT protocol when dealing with properties in a node. - // Lists appear not to receive a composite type label, - // which is why we have to handle them specifically here. - // @see https://github.com/neo4j/neo4j/issues/12858 - if (is_array($element)) { - yield $key => new CypherList($element); - } else { - yield $key => $this->translateJoltType($element); - } - } - } - ); - } - - private function translateList(array $value): CypherList - { - return new CypherList( - function () use ($value) { - /** @var stdClass|null $element */ - foreach ($value as $element) { - yield $this->translateJoltType($element); - } - } - ); - } - - /** - * @param list $value - */ - private function translatePath(array $value): Path - { - $nodes = []; - /** @var list $relations */ - $relations = []; - $ids = []; - foreach ($value as $nodeOrRelation) { - /** @var Node|Relationship $nodeOrRelation */ - $nodeOrRelation = $this->translateJoltType($nodeOrRelation); - - if ($nodeOrRelation instanceof Relationship) { - $relations[] = $nodeOrRelation; - } else { - $nodes[] = $nodeOrRelation; - } - - $ids[] = $nodeOrRelation->getId(); - } - - return new Path(new CypherList($nodes), new CypherList($relations), new CypherList($ids)); - } - - /** - * @param array{0: int, 1: list, 2: stdClass} $value - */ - private function translateNode(array $value): Node - { - return new Node($value[0], new CypherList($value[1]), $this->translateMap($value[2]), null); - } - - /** - * @param array{0:int, 1: int, 2: string, 3:int, 4: stdClass} $value - */ - private function translateRightRelationship(array $value): Relationship - { - return new Relationship($value[0], $value[1], $value[3], $value[2], $this->translateMap($value[4]), null); - } - - /** - * @param array{0:int, 1: int, 2: string, 3:int, 4: stdClass} $value - */ - private function translateLeftRelationship(array $value): Relationship - { - return new Relationship($value[0], $value[3], $value[1], $value[2], $this->translateMap($value[4]), null); - } - - private function translateBinary(): Closure - { - throw new UnexpectedValueException('Binary data has not been implemented'); - } - - private const TIME_REGEX = '(?\d{2}):(?\d{2}):(?\d{2})((\.)(?\d+))?'; - private const DATE_REGEX = '(?[\-−]?\d+-\d{2}-\d{2})'; - private const ZONE_REGEX = '(?.+)'; - - /** - * @psalm-suppress ImpureMethodCall - * @psalm-suppress ImpureFunctionCall - * @psalm-suppress PossiblyFalseReference - */ - private function translateDateTime(string $datetime): Date|LocalDateTime|LocalTime|DateTime|Duration|Time - { - if (preg_match('/^'.self::DATE_REGEX.'$/u', $datetime, $matches)) { - $days = $this->daysFromMatches($matches); - - return new Date($days); - } - - if (preg_match('/^'.self::TIME_REGEX.'$/u', $datetime, $matches)) { - $nanoseconds = $this->nanosecondsFromMatches($matches); - - return new LocalTime($nanoseconds); - } - - if (preg_match('/^'.self::TIME_REGEX.self::ZONE_REGEX.'$/u', $datetime, $matches)) { - $nanoseconds = $this->nanosecondsFromMatches($matches); - - $offset = $this->offsetFromMatches($matches); - - return new Time($nanoseconds, $offset); - } - - if (preg_match('/^'.self::DATE_REGEX.'T'.self::TIME_REGEX.'$/u', $datetime, $matches)) { - $nanoseconds = $this->nanosecondsFromMatches($matches); - $seconds = $this->secondsInDaysFromMatches($matches); - - [$seconds, $nanoseconds] = $this->addNanoSecondsToSeconds($nanoseconds, $seconds); - - return new LocalDateTime($seconds, $nanoseconds); - } - - if (preg_match('/^'.self::DATE_REGEX.'T'.self::TIME_REGEX.self::ZONE_REGEX.'$/u', $datetime, $matches)) { - $nanoseconds = $this->nanosecondsFromMatches($matches); - $seconds = $this->secondsInDaysFromMatches($matches); - - [$seconds, $nanoseconds] = $this->addNanoSecondsToSeconds($nanoseconds, $seconds); - - $offset = $this->offsetFromMatches($matches); - - return new DateTime($seconds, $nanoseconds, $offset, true); - } - - if (str_starts_with($datetime, 'P')) { - return $this->durationFromFormat($datetime); - } - - throw new UnexpectedValueException(sprintf('Could not handle date/time "%s"', $datetime)); - } - - private function nanosecondsFromMatches(array $matches): int - { - /** @var array{0: string, hours: string, minutes: string, seconds: string, nanoseconds?: string} $matches */ - ['hours' => $hours, 'minutes' => $minutes, 'seconds' => $seconds] = $matches; - $seconds = (((int) $hours) * 60 * 60) + (((int) $minutes) * 60) + ((int) $seconds); - - $nanoseconds = $matches['nanoseconds'] ?? '0'; - $nanoseconds = str_pad($nanoseconds, 9, '0', STR_PAD_RIGHT); - - return $seconds * 1000 * 1000 * 1000 + (int) $nanoseconds; - } - - private function offsetFromMatches(array $matches): int - { - /** @var array{zone: string} $matches */ - $zone = $matches['zone']; - - if (preg_match('/(\d{2}):(\d{2})/', $zone, $matches)) { - /** @var array{0: string, 1: string, 2: string} $matches */ - return ((int) $matches[1]) * 60 * 60 + (int) $matches[2] * 60; - } - - return 0; - } - - private function daysFromMatches(array $matches): int - { - /** @var array{date: string} $matches */ - $date = DateTimeImmutable::createFromFormat('Y-m-d', $matches['date']); - if ($date === false) { - throw new RuntimeException(sprintf('Cannot create DateTime from "%s" in format "Y-m-d"', $matches['date'])); - } - - /** @psalm-suppress ImpureMethodCall */ - return (int) $date->diff(new DateTimeImmutable('@0'))->format('%a'); - } - - private function secondsInDaysFromMatches(array $matches): int - { - /** @var array{date: string} $matches */ - $date = DateTimeImmutable::createFromFormat(DATE_ATOM, $matches['date'].'T00:00:00+00:00'); - if ($date === false) { - throw new RuntimeException(sprintf('Cannot create DateTime from "%s" in format "Y-m-d"', $matches['date'])); - } - - return $date->getTimestamp(); - } - - /** - * @return array{0: int, 1: int} - */ - private function addNanoSecondsToSeconds(int $nanoseconds, int $seconds): array - { - $seconds += (int) ($nanoseconds / 1000 / 1000 / 1000); - $nanoseconds %= 1_000_000_000; - - return [$seconds, $nanoseconds]; - } - - /** - * @psalm-suppress ImpureMethodCall - */ - private function durationFromFormat(string $datetime): Duration - { - $nanoseconds = 0; - // PHP date interval does not understand fractions of a second. - if (preg_match('/\.(?\d+)S/u', $datetime, $matches)) { - /** @var array{0: string, nanoseconds: string} $matches */ - $nanoseconds = (int) str_pad($matches['nanoseconds'], 9, '0', STR_PAD_RIGHT); - - $datetime = str_replace($matches[0], 'S', $datetime); - } - - $interval = new DateInterval($datetime); - $months = (int) $interval->format('%y') * 12 + (int) $interval->format('%m'); - $days = (int) $interval->format('%d'); - $seconds = (int) $interval->format('%h') * 60 * 60 + (int) $interval->format('%i') * 60 + (int) $interval->format('%s'); - - return new Duration($months, $days, $seconds, $nanoseconds); - } -} diff --git a/src/Formatter/Specialised/LegacyHttpOGMTranslator.php b/src/Formatter/Specialised/LegacyHttpOGMTranslator.php deleted file mode 100644 index e66a71eb..00000000 --- a/src/Formatter/Specialised/LegacyHttpOGMTranslator.php +++ /dev/null @@ -1,536 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Formatter\Specialised; - -use function array_combine; -use function array_key_exists; -use function count; -use function date; - -use DateInterval; -use DateTimeImmutable; -use Exception; - -use function explode; -use function is_array; -use function is_object; -use function is_string; -use function json_encode; - -use const JSON_THROW_ON_ERROR; - -use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Contracts\PointInterface; -use Laudis\Neo4j\Formatter\OGMFormatter; -use Laudis\Neo4j\Types\Cartesian3DPoint; -use Laudis\Neo4j\Types\CartesianPoint; -use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; -use Laudis\Neo4j\Types\Date; -use Laudis\Neo4j\Types\DateTime; -use Laudis\Neo4j\Types\Duration; -use Laudis\Neo4j\Types\LocalDateTime; -use Laudis\Neo4j\Types\LocalTime; -use Laudis\Neo4j\Types\Node; -use Laudis\Neo4j\Types\Path; -use Laudis\Neo4j\Types\Relationship; -use Laudis\Neo4j\Types\Time; -use Laudis\Neo4j\Types\UnboundRelationship; -use Laudis\Neo4j\Types\WGS843DPoint; -use Laudis\Neo4j\Types\WGS84Point; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use RuntimeException; - -use function sprintf; - -use stdClass; - -use function str_pad; -use function substr; - -use UnexpectedValueException; - -/** - * @psalm-import-type OGMTypes from OGMFormatter - * - * @psalm-immutable - */ -final class LegacyHttpOGMTranslator -{ - /** - * @psalm-mutation-free - * - * @return CypherList>> - */ - public function formatHttpResult( - ResponseInterface $response, - stdClass $body, - ConnectionInterface $connection, - float $resultsAvailableAfter, - float $resultsConsumedAfter, - iterable $statements - ): CypherList { - /** @var list>> $tbr */ - $tbr = []; - - /** @var list $results */ - $results = $body->results; - foreach ($results as $result) { - $tbr[] = $this->translateResult($result); - } - - return new CypherList($tbr); - } - - public function decorateRequest(RequestInterface $request): RequestInterface - { - return $request; - } - - /** - * @return array{resultDataContents?: list<'GRAPH'|'ROW'|'REST'>, includeStats?:bool} - */ - public function statementConfigOverride(): array - { - return [ - 'resultDataContents' => ['ROW', 'GRAPH'], - ]; - } - - /** - * @throws Exception - * - * @return CypherList> - */ - public function translateResult(stdClass $result): CypherList - { - /** @var list> $tbr */ - $tbr = []; - - /** @var list $columns */ - $columns = $result->columns; - /** @var list $datas */ - $datas = $result->data; - foreach ($datas as $data) { - $meta = HttpMetaInfo::createFromData($data); - - /** @var list $row */ - $row = $data->row; - $row = array_combine($columns, $row); - $tbr[] = $this->translateCypherMap($row, $meta)[0]; - } - - return new CypherList($tbr); - } - - /** - * @param array $row - * - * @return array{0: CypherMap, 1: HttpMetaInfo} - */ - public function translateCypherMap(array $row, HttpMetaInfo $meta): array - { - /** @var array $record */ - $record = []; - foreach ($row as $key => $value) { - [$translation, $meta] = $this->translateValue($value, $meta); - - $record[$key] = $translation; - } - - return [new CypherMap($record), $meta]; - } - - /** - * @param array|scalar|stdClass|null $value - * - * @return array{0: OGMTypes, 1: HttpMetaInfo} - * - * @psalm-suppress MixedArgumentTypeCoercion - * @psalm-suppress MixedArgument - * @psalm-suppress MixedAssignment - * @psalm-suppress InvalidReturnStatement - * @psalm-suppress ArgumentTypeCoercion - * @psalm-suppress InvalidReturnType - */ - private function translateValue(float|array|bool|int|string|stdClass|null $value, HttpMetaInfo $meta): array - { - if (is_object($value)) { - return $this->translateObject($value, $meta); - } - - if (is_array($value)) { - if ($meta->getCurrentType() === 'path') { - /** - * There are edge cases where multiple paths are wrapped in a list. - * - * @see OGMFormatterIntegrationTest::testPathMultiple for an example - */ - if (array_key_exists(0, $value) && is_array($value[0])) { - $tbr = []; - foreach ($value as $path) { - $tbr[] = $this->path($path, $meta->withNestedMeta()); - $meta = $meta->incrementMeta(); - } - - return [new CypherList($tbr), $meta]; - } - - $tbr = $this->path($value, $meta->withNestedMeta()); - $meta = $meta->incrementMeta(); - - return [$tbr, $meta]; - } - - return $this->translateCypherList($value, $meta); - } - - if (is_string($value)) { - return $this->translateString($value, $meta); - } - - return [$value, $meta->incrementMeta()]; - } - - /** - * @return array{0: Cartesian3DPoint|CartesianPoint|CypherList|CypherMap|Node|Relationship|WGS843DPoint|WGS84Point|Path, 1: HttpMetaInfo} - * - * @psalm-suppress MixedArgument - * @psalm-suppress MixedArgumentTypeCoercion - */ - private function translateObject(stdClass $value, HttpMetaInfo $meta): array - { - $type = $meta->getCurrentType(); - if ($type === 'relationship') { - /** @var stdClass $relationship */ - $relationship = $meta->getCurrentRelationship(); - - return $this->relationship($relationship, $meta); - } - - if ($type === 'point') { - return [$this->translatePoint($value), $meta]; - } - - if ($type === 'node') { - $node = $meta->currentNode(); - if ($node && json_encode($value, JSON_THROW_ON_ERROR) === json_encode($node->properties, JSON_THROW_ON_ERROR)) { - $meta = $meta->incrementMeta(); - $map = $this->translateProperties((array) $node->properties); - - return [new Node((int) $node->id, new CypherList($node->labels), $map, null), $meta]; - } - } - - return $this->translateCypherMap((array) $value, $meta); - } - - /** - * @param array $properties - * - * @return CypherMap - */ - private function translateProperties(array $properties): CypherMap - { - $tbr = []; - foreach ($properties as $key => $value) { - if ($value instanceof stdClass) { - /** @var array $castedValue */ - $castedValue = (array) $value; - $tbr[$key] = $this->translateProperties($castedValue); - } elseif (is_array($value)) { - /** @var array $value */ - $tbr[$key] = new CypherList($this->translateProperties($value)); - } else { - $tbr[$key] = $value; - } - } - /** @var CypherMap */ - return new CypherMap($tbr); - } - - /** - * @psalm-suppress MixedArgument - * @psalm-suppress MixedArgumentTypeCoercion - * - * @return array{0: Relationship, 1: HttpMetaInfo} - */ - private function relationship(stdClass $relationship, HttpMetaInfo $meta): array - { - $meta = $meta->incrementMeta(); - $map = $this->translateProperties((array) $relationship->properties); - - $tbr = new Relationship( - (int) $relationship->id, - (int) $relationship->startNode, - (int) $relationship->endNode, - $relationship->type, - $map, - null - ); - - return [$tbr, $meta]; - } - - /** - * @param list $value - * - * @return array{0: CypherList, 1: HttpMetaInfo} - */ - private function translateCypherList(array $value, HttpMetaInfo $meta): array - { - /** @var array $tbr */ - $tbr = []; - foreach ($value as $x) { - [$x, $meta] = $this->translateValue($x, $meta); - $tbr[] = $x; - } - - return [new CypherList($tbr), $meta]; - } - - /** - * @param list $value - */ - private function path(array $value, HttpMetaInfo $meta): Path - { - /** @var list $nodes */ - $nodes = []; - /** @var list $ids */ - $ids = []; - /** @var list $rels */ - $rels = []; - - foreach ($value as $x) { - /** @var stdClass $currentMeta */ - $currentMeta = $meta->currentMeta(); - /** @var int $id */ - $id = $currentMeta->id; - $ids[] = $id; - [$x, $meta] = $this->translateObject($x, $meta); - if ($x instanceof Node) { - $nodes[] = $x; - } elseif ($x instanceof Relationship) { - $rels[] = new UnboundRelationship($x->getId(), $x->getType(), $x->getProperties(), null); - } - } - - return new Path(new CypherList($nodes), new CypherList($rels), new CypherList($ids)); - } - - /** - * @return CartesianPoint|Cartesian3DPoint|WGS843DPoint|WGS84Point - */ - private function translatePoint(stdClass $value): PointInterface - { - /** @var stdClass $crs */ - $crs = $value->crs; - /** @var array{0: float, 1: float, 2:float} $coordinates */ - $coordinates = $value->coordinates; - /** @var int $srid */ - $srid = $crs->srid; - if ($srid === CartesianPoint::SRID) { - return new CartesianPoint( - $coordinates[0], - $coordinates[1], - ); - } - if ($srid === Cartesian3DPoint::SRID) { - return new Cartesian3DPoint( - $coordinates[0], - $coordinates[1], - $coordinates[2], - ); - } - if ($srid === WGS84Point::SRID) { - return new WGS84Point( - $coordinates[0], - $coordinates[1], - ); - } - if ($srid === WGS843DPoint::SRID) { - return new WGS843DPoint( - $coordinates[0], - $coordinates[1], - $coordinates[2], - ); - } - /** @var string $name */ - $name = $crs->name; - throw new UnexpectedValueException('A point with srid '.$srid.' and name '.$name.' has been returned, which has not been implemented.'); - } - - /** - * @throws Exception - * - * @return array{0: string|Date|DateTime|Duration|LocalDateTime|LocalTime|Time, 1: HttpMetaInfo} - */ - public function translateString(string $value, HttpMetaInfo $meta): array - { - switch ($meta->getCurrentType()) { - case 'duration': - $meta = $meta->incrementMeta(); - $tbr = [$this->translateDuration($value), $meta]; - break; - case 'datetime': - $meta = $meta->incrementMeta(); - $tbr = [$this->translateDateTime($value), $meta]; - break; - case 'date': - $meta = $meta->incrementMeta(); - $tbr = [$this->translateDate($value), $meta]; - break; - case 'time': - $meta = $meta->incrementMeta(); - $tbr = [$this->translateTime($value), $meta]; - break; - case 'localdatetime': - $meta = $meta->incrementMeta(); - $tbr = [$this->translateLocalDateTime($value), $meta]; - break; - case 'localtime': - $meta = $meta->incrementMeta(); - $tbr = [$this->translateLocalTime($value), $meta]; - break; - default: - $tbr = [$value, $meta->incrementMeta()]; - break; - } - - return $tbr; - } - - /** - * @throws Exception - */ - private function translateDuration(string $value): Duration - { - /** @psalm-suppress ImpureFunctionCall false positive in version php 7.4 */ - if (str_contains($value, '.')) { - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$format, $secondsFraction] = explode('.', $value); - $nanoseconds = (int) substr($secondsFraction, 6); - $microseconds = (int) str_pad((string) ((int) substr($secondsFraction, 0, 6)), 6, '0'); - $interval = new DateInterval($format.'S'); - $x = new DateTimeImmutable(); - /** @psalm-suppress PossiblyFalseReference */ - $interval = $x->add($interval)->modify('+'.$microseconds.' microseconds')->diff($x); - } else { - $nanoseconds = 0; - $interval = new DateInterval($value); - } - - $months = $interval->y * 12 + $interval->m; - $days = $interval->d; - $seconds = $interval->h * 60 * 60 + $interval->i * 60 + $interval->s; - $nanoseconds = (int) ($interval->f * 1_000_000_000) + $nanoseconds; - - return new Duration($months, $days, $seconds, $nanoseconds); - } - - private function translateDate(string $value): Date - { - $epoch = new DateTimeImmutable('@0'); - $dateTime = DateTimeImmutable::createFromFormat('Y-m-d', $value); - if ($dateTime === false) { - throw new RuntimeException(sprintf('Could not create date from format "Y-m-d" and %s', $value)); - } - - $diff = $dateTime->diff($epoch); - - /** @psalm-suppress ImpureMethodCall */ - return new Date((int) $diff->format('%a')); - } - - private function translateTime(string $value): Time - { - $value = substr($value, 0, 5); - $values = explode(':', $value); - - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - return new Time((((int) $values[0]) * 60 * 60 + ((int) $values[1]) * 60) * 1_000_000_000, 0); - } - - /** - * @throws Exception - */ - private function translateDateTime(string $value): DateTime - { - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$date, $time] = explode('T', $value); - $tz = null; - if (str_contains($time, '+')) { - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$time, $timezone] = explode('+', $time); - - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$tzHours, $tzMinutes] = explode(':', $timezone); - $tz = (int) $tzHours * 60 * 60 + (int) $tzMinutes * 60; - } - - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$time, $milliseconds] = explode('.', $time); - - $dateTime = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date.' '.$time); - if ($dateTime === false) { - throw new RuntimeException(sprintf('Could not create date from format "Y-m-d H:i:s" and %s', $date.' '.$time)); - } - - if ($tz !== null) { - return new DateTime($dateTime->getTimestamp(), (int) $milliseconds * 1_000_000, $tz, true); - } - - return new DateTime($dateTime->getTimestamp(), (int) $milliseconds * 1_000_000, 0, true); - } - - private function translateLocalDateTime(string $value): LocalDateTime - { - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$date, $time] = explode('T', $value); - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - [$time, $milliseconds] = explode('.', $time); - - $dateTime = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $date.' '.$time); - if ($dateTime === false) { - throw new RuntimeException(sprintf('Could not create date from format "Y-m-d H:i:s" and %s', $date.' '.$time)); - } - - return new LocalDateTime($dateTime->getTimestamp(), (int) $milliseconds * 1_000_000); - } - - /** - * @psalm-suppress all - * - * @throws Exception - */ - private function translateLocalTime(string $value): LocalTime - { - $timestamp = (new DateTimeImmutable($value))->getTimestamp(); - - $hours = (int) date('H', $timestamp); - $minutes = (int) date('i', $timestamp); - $seconds = (int) date('s', $timestamp); - $milliseconds = 0; - - $values = explode('.', $value); - if (count($values) > 1) { - $milliseconds = $values[1]; - } - - $totalSeconds = ($hours * 3600) + ($minutes * 60) + $seconds + ($milliseconds / 1000); - - return new LocalTime((int) $totalSeconds * 1_000_000_000); - } -} diff --git a/src/Formatter/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php index c8864f76..27b29842 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -18,7 +18,6 @@ use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltResult; -use Laudis\Neo4j\Contracts\ConnectionInterface; use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\DatabaseInfo; @@ -28,17 +27,11 @@ use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\SummaryCounters; use Laudis\Neo4j\Enum\QueryTypeEnum; -use Laudis\Neo4j\Http\HttpConnection; use Laudis\Neo4j\Types\CypherList; use Laudis\Neo4j\Types\CypherMap; use function microtime; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use stdClass; -use UnexpectedValueException; - /** * Decorates the result of the provided format with an extensive summary. * @@ -67,67 +60,6 @@ public function __construct( private readonly OGMFormatter $formatter ) {} - /** - * @param CypherList> $results - * - * @return SummarizedResult> - * - * @psalm-mutation-free - */ - public function formatHttpStats(stdClass $response, HttpConnection $connection, Statement $statement, float $resultAvailableAfter, float $resultConsumedAfter, CypherList $results): SummarizedResult - { - if (isset($response->summary) && $response->summary instanceof stdClass) { - /** @var stdClass $stats */ - $stats = $response->summary->stats; - } elseif (isset($response->stats)) { - /** @var stdClass $stats */ - $stats = $response->stats; - } else { - throw new UnexpectedValueException('No stats found in the response set'); - } - - /** - * @psalm-suppress MixedPropertyFetch - * @psalm-suppress MixedArgument - */ - $counters = new SummaryCounters( - $stats->nodes_created ?? 0, - $stats->nodes_deleted ?? 0, - $stats->relationships_created ?? 0, - $stats->relationships_deleted ?? 0, - $stats->properties_set ?? 0, - $stats->labels_added ?? 0, - $stats->labels_removed ?? 0, - $stats->indexes_added ?? 0, - $stats->indexes_removed ?? 0, - $stats->constraints_added ?? 0, - $stats->constraints_removed ?? 0, - $stats->contains_updates ?? false, - $stats->contains_system_updates ?? false, - $stats->system_updates ?? 0, - ); - - $summary = new ResultSummary( - $counters, - $connection->getDatabaseInfo(), - new CypherList(), - null, - null, - $statement, - QueryTypeEnum::fromCounters($counters), - $resultAvailableAfter, - $resultConsumedAfter, - new ServerInfo( - $connection->getServerAddress(), - $connection->getProtocol(), - $connection->getServerAgent() - ) - ); - - /** @var SummarizedResult> */ - return new SummarizedResult($summary, $results); - } - /** * @param array{stats?: BoltCypherStats}&array $response * @@ -201,45 +133,4 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection */ return (new SummarizedResult($summary, $formattedResult))->withCacheLimit($result->getFetchSize()); } - - /** - * @psalm-mutation-free - * - * @psalm-suppress ImpureMethodCall - */ - public function formatHttpResult(ResponseInterface $response, stdClass $body, HttpConnection $connection, float $resultsAvailableAfter, float $resultsConsumedAfter, iterable $statements): CypherList - { - /** @var list>> */ - $tbr = []; - - $toDecorate = $this->formatter->formatHttpResult($response, $body, $connection, $resultsAvailableAfter, $resultsConsumedAfter, $statements); - $i = 0; - foreach ($statements as $statement) { - /** @var list $results */ - $results = $body->results; - $result = $results[$i]; - $tbr[] = $this->formatHttpStats($result, $connection, $statement, $resultsAvailableAfter, $resultsConsumedAfter, $toDecorate->get($i)); - ++$i; - } - - return new CypherList($tbr); - } - - /** - * @psalm-mutation-free - */ - public function decorateRequest(RequestInterface $request, ConnectionInterface $connection): RequestInterface - { - return $this->formatter->decorateRequest($request, $connection); - } - - /** - * @psalm-mutation-free - */ - public function statementConfigOverride(ConnectionInterface $connection): array - { - return array_merge($this->formatter->statementConfigOverride($connection), [ - 'includeStats' => true, - ]); - } } diff --git a/src/Http/HttpConnection.php b/src/Http/HttpConnection.php deleted file mode 100644 index a7a0c8f3..00000000 --- a/src/Http/HttpConnection.php +++ /dev/null @@ -1,165 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use Laudis\Neo4j\Common\ConnectionConfiguration; -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Databags\DatabaseInfo; -use Laudis\Neo4j\Enum\AccessMode; -use Laudis\Neo4j\Enum\ConnectionProtocol; -use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\UriInterface; - -/** - * @implements ConnectionInterface - */ -final class HttpConnection implements ConnectionInterface -{ - private bool $isOpen = true; - - /** - * @psalm-mutation-free - */ - public function __construct( - /** @psalm-readonly */ - private readonly ClientInterface $client, - /** @psalm-readonly */ - private readonly ConnectionConfiguration $config, - private readonly AuthenticateInterface $authenticate, - private readonly string $userAgent - ) {} - - /** - * @psalm-mutation-free - */ - public function getImplementation(): ClientInterface - { - return $this->client; - } - - /** - * @psalm-mutation-free - */ - public function getServerAgent(): string - { - return $this->config->getServerAgent(); - } - - /** - * @psalm-mutation-free - */ - public function getServerAddress(): UriInterface - { - return $this->config->getServerAddress(); - } - - /** - * @psalm-mutation-free - */ - public function getServerVersion(): string - { - return $this->config->getServerVersion(); - } - - /** - * @psalm-mutation-free - */ - public function getProtocol(): ConnectionProtocol - { - return $this->config->getProtocol(); - } - - /** - * @psalm-mutation-free - */ - public function getAccessMode(): AccessMode - { - return $this->config->getAccessMode(); - } - - /** - * @psalm-mutation-free - */ - public function getDatabaseInfo(): DatabaseInfo - { - return $this->config->getDatabaseInfo() ?? new DatabaseInfo(''); - } - - /** - * @psalm-mutation-free - */ - public function isOpen(): bool - { - return $this->isOpen; - } - - /** - * @psalm-external-mutation-free - */ - public function open(): void - { - $this->isOpen = true; - } - - /** - * @psalm-external-mutation-free - */ - public function close(): void - { - $this->isOpen = false; - } - - public function reset(): void - { - // Cannot reset a stateless protocol - } - - public function setTimeout(float $timeout): void - { - // Impossible to actually set a timeout with PSR definition - } - - /** - * @psalm-immutable - */ - public function getAuthentication(): AuthenticateInterface - { - return $this->authenticate; - } - - /** - * @psalm-mutation-free - */ - public function getServerState(): string - { - return 'UNKNOWN'; - } - - /** - * @psalm-mutation-free - */ - public function getEncryptionLevel(): string - { - return $this->config->getEncryptionLevel(); - } - - /** - * @psalm-mutation-free - */ - public function getUserAgent(): string - { - return $this->userAgent; - } -} diff --git a/src/Http/HttpConnectionPool.php b/src/Http/HttpConnectionPool.php deleted file mode 100644 index 2c0e3929..00000000 --- a/src/Http/HttpConnectionPool.php +++ /dev/null @@ -1,135 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use Generator; - -use function json_encode; - -use Laudis\Neo4j\Common\ConnectionConfiguration; -use Laudis\Neo4j\Common\Resolvable; -use Laudis\Neo4j\Common\Uri; -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Contracts\ConnectionPoolInterface; -use Laudis\Neo4j\Databags\DatabaseInfo; -use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Enum\ConnectionProtocol; -use Laudis\Neo4j\Formatter\BasicFormatter; -use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\StreamFactoryInterface; -use Psr\Http\Message\UriInterface; -use Throwable; - -/** - * @implements ConnectionPoolInterface - */ -final class HttpConnectionPool implements ConnectionPoolInterface -{ - /** - * @param Resolvable $client - * @param Resolvable $requestFactory - * @param Resolvable $streamFactory - * @param Resolvable $tsxUrl - * - * @psalm-mutation-free - */ - public function __construct( - /** - * @psalm-readonly - */ - private readonly Resolvable $client, - /** - * @psalm-readonly - */ - private readonly Resolvable $requestFactory, - /** - * @psalm-readonly - */ - private readonly Resolvable $streamFactory, - private readonly AuthenticateInterface $auth, - private readonly string $userAgent, - private readonly Resolvable $tsxUrl - ) {} - - public function acquire(SessionConfiguration $config): Generator - { - yield 0.0; - - $uri = Uri::create($this->tsxUrl->resolve()); - $request = $this->requestFactory->resolve()->createRequest('POST', $uri); - - $path = $request->getUri()->getPath().'/commit'; - $uri = $uri->withPath($path); - $request = $request->withUri($uri); - - $body = json_encode([ - 'statements' => [ - [ - 'statement' => <<<'CYPHER' -CALL dbms.components() -YIELD name, versions, edition -RETURN name, versions, edition -CYPHER - , - ], - ], - 'resultDataContents' => [], - 'includeStats' => false, - ], JSON_THROW_ON_ERROR); - - $request = $request->withBody($this->streamFactory->resolve()->createStream($body)); - - $response = $this->client->resolve()->sendRequest($request); - $data = HttpHelper::interpretResponse($response); - /** @var array{0: array{name: string, versions: list, edition: string}} $results */ - $results = (new BasicFormatter())->formatHttpResult($response, $data, null)->first(); - - $version = $results[0]['versions'][0] ?? ''; - - $config = new ConnectionConfiguration( - $results[0]['name'].'-'.$results[0]['edition'].'/'.$version, - $uri, - $version, - ConnectionProtocol::HTTP(), - $config->getAccessMode(), - new DatabaseInfo($config->getDatabase() ?? ''), - '' - ); - - return new HttpConnection($this->client->resolve(), $config, $this->auth, $this->userAgent); - } - - public function canConnect(UriInterface $uri, AuthenticateInterface $authenticate, ?string $userAgent = null): bool - { - $request = $this->requestFactory->resolve()->createRequest('GET', $uri); - $client = $this->client->resolve(); - - try { - return $client->sendRequest($request)->getStatusCode() === 200; - } catch (Throwable) { - return false; - } - } - - public function release(ConnectionInterface $connection): void - { - // Nothing to release in the current HTTP Protocol implementation - } - - public function close(): void - { - // Nothing to close in the current HTTP Protocol implementation - } -} diff --git a/src/Http/HttpDriver.php b/src/Http/HttpDriver.php deleted file mode 100644 index d798d347..00000000 --- a/src/Http/HttpDriver.php +++ /dev/null @@ -1,206 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use function is_string; - -use Laudis\Neo4j\Authentication\Authenticate; -use Laudis\Neo4j\Common\Resolvable; -use Laudis\Neo4j\Common\Uri; -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Contracts\DriverInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; -use Laudis\Neo4j\Contracts\SessionInterface; -use Laudis\Neo4j\Databags\DriverConfiguration; -use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Formatter\OGMFormatter; -use Psr\Http\Message\StreamFactoryInterface; -use Psr\Http\Message\UriInterface; - -use function str_replace; -use function uniqid; - -/** - * @template T - * - * @implements DriverInterface - * - * @psalm-import-type OGMResults from OGMFormatter - */ -final class HttpDriver implements DriverInterface -{ - private readonly string $key; - - /** - * @psalm-mutation-free - * - * @param FormatterInterface $formatter - */ - public function __construct( - private readonly UriInterface $uri, - private readonly DriverConfiguration $config, - private readonly FormatterInterface $formatter, - private readonly AuthenticateInterface $auth - ) { - /** @psalm-suppress ImpureFunctionCall */ - $this->key = uniqid(); - } - - /** - * @template U - * - * @param FormatterInterface $formatter - * - * @return ( - * func_num_args() is 4 - * ? self - * : self - * ) - * - * @pure - */ - public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null, ?FormatterInterface $formatter = null): self - { - if (is_string($uri)) { - $uri = Uri::create($uri); - } - - $configuration ??= DriverConfiguration::default(); - if ($formatter !== null) { - return new self( - $uri, - $configuration, - $formatter, - $authenticate ?? Authenticate::fromUrl($uri, $configuration->getLogger()) - ); - } - - return new self( - $uri, - $configuration, - OGMFormatter::create(), - $authenticate ?? Authenticate::fromUrl($uri, $configuration->getLogger()) - ); - } - - /** - * @psalm-external-mutation-free - */ - public function createSession(?SessionConfiguration $config = null): SessionInterface - { - $factory = $this->resolvableFactory(); - $config ??= SessionConfiguration::default(); - $config = $config->merge(SessionConfiguration::fromUri($this->uri, null)); - $streamFactoryResolve = $this->streamFactory(); - - $tsxUrl = $this->tsxUrl($config); - - return new HttpSession( - $streamFactoryResolve, - $this->getHttpConnectionPool($tsxUrl), - $config, - $this->formatter, - $factory, - $tsxUrl, - $this->auth, - $this->config->getUserAgent() - ); - } - - public function verifyConnectivity(?SessionConfiguration $config = null): bool - { - $config ??= SessionConfiguration::default(); - - return $this->getHttpConnectionPool($this->tsxUrl($config)) - ->canConnect($this->uri, $this->auth); - } - - /** - * @param Resolvable $tsxUrl - * - * @psalm-mutation-free - */ - private function getHttpConnectionPool(Resolvable $tsxUrl): HttpConnectionPool - { - return new HttpConnectionPool( - Resolvable::once($this->key.':client', fn () => $this->config->getHttpPsrBindings()->getClient()), - $this->resolvableFactory(), - $this->streamFactory(), - $this->auth, - $this->config->getUserAgent(), - $tsxUrl - ); - } - - /** - * @return Resolvable - * - * @psalm-mutation-free - */ - private function resolvableFactory(): Resolvable - { - return Resolvable::once($this->key.':requestFactory', function () { - $bindings = $this->config->getHttpPsrBindings(); - - return new RequestFactory($bindings->getRequestFactory(), $this->auth, $this->uri, $this->config->getUserAgent()); - }); - } - - /** - * @return Resolvable - * - * @psalm-mutation-free - */ - private function streamFactory(): Resolvable - { - return Resolvable::once($this->key.':streamFactory', fn () => $this->config->getHttpPsrBindings()->getStreamFactory()); - } - - /** - * @return Resolvable - * - * @psalm-mutation-free - */ - private function tsxUrl(SessionConfiguration $config): Resolvable - { - return Resolvable::once($this->key.':tsxUrl', function () use ($config) { - $database = $config->getDatabase() ?? 'neo4j'; - $request = $this->resolvableFactory()->resolve()->createRequest('GET', $this->uri); - $client = $this->config->getHttpPsrBindings()->getClient(); - - $response = $client->sendRequest($request); - - $discovery = HttpHelper::interpretResponse($response); - /** @var string|null */ - $version = $discovery->neo4j_version ?? null; - - if ($version === null) { - /** @var string */ - $uri = $discovery->data; - $request = $request->withUri(Uri::create($uri)); - $discovery = HttpHelper::interpretResponse($client->sendRequest($request)); - } - - /** @var string */ - $tsx = $discovery->transaction; - - return str_replace('{databaseName}', $database, $tsx); - }); - } - - public function closeConnections(): void - { - // Nothing to close in the current HTTP Protocol implementation - } -} diff --git a/src/Http/HttpHelper.php b/src/Http/HttpHelper.php deleted file mode 100644 index 822e6880..00000000 --- a/src/Http/HttpHelper.php +++ /dev/null @@ -1,220 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use function array_key_first; -use function array_merge; -use function count; -use function json_decode; -use function json_encode; - -use const JSON_THROW_ON_ERROR; - -use JsonException; -use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; -use Laudis\Neo4j\Databags\Neo4jError; -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Exception\Neo4jException; -use Laudis\Neo4j\ParameterHelper; -use Psr\Http\Message\ResponseInterface; -use RuntimeException; -use stdClass; -use UnexpectedValueException; - -/** - * Helper functions for the http protocol. - * - * @psalm-import-type CypherResponseSet from \Laudis\Neo4j\Contracts\FormatterInterface - */ -final class HttpHelper -{ - /** - * Checks the response and interprets it. Throws if an error is detected. - * - * @throws JsonException - * @throws RuntimeException - * @throws UnexpectedValueException - */ - public static function interpretResponse(ResponseInterface $response): stdClass - { - if ($response->getStatusCode() >= 500) { - throw new RuntimeException('HTTP Error: '.$response->getReasonPhrase()); - } - - $contents = $response->getBody()->getContents(); - - /** @var stdClass $body */ - // Jolt is a Json sequence (rfc 7464), so it starts with a RS control character "\036" - if ($contents[0] === "\036") { - $body = self::getJoltBody($contents); - } else { - // If not Jolt, assume it is Json - $body = self::getJsonBody($contents); - } - - $errors = []; - /** @var list $bodyErrors */ - $bodyErrors = $body->errors ?? []; - foreach ($bodyErrors as $error) { - /** @var string */ - $code = $error->code; - /** @var string */ - $message = $error->message; - $errors[] = Neo4jError::fromMessageAndCode($code, $message); - } - - if (count($errors) !== 0) { - throw new Neo4jException($errors); - } - - return $body; - } - - /** - * @throws JsonException - */ - public static function getJsonBody(string $contents): stdClass - { - /** @var stdClass */ - return json_decode($contents, false, 512, JSON_THROW_ON_ERROR); - } - - /** - * Converts a Jolt input (with JSON sequence separators) into a stdClass that contains the data of all jsons of the sequence. - * - * @throws JsonException - * @throws RuntimeException - * @throws UnexpectedValueException - * - * @psalm-suppress MixedAssignment - * @psalm-suppress MixedArrayAssignment - * @psalm-suppress MixedPropertyFetch - * @psalm-suppress MixedArgument - */ - public static function getJoltBody(string $contents): stdClass - { - // Split json sequence in single jsons, split on json sequence separators. - $contents = explode("\036", $contents); - - // Drop first (empty) string. - array_shift($contents); - - // stdClass to capture all the jsons - $rtr = new stdClass(); - $rtr->results = []; - - // stdClass to capture the jsons of the results of a single statement that has been sent. - $data = new stdClass(); - $data->data = []; - - foreach ($contents as $content) { - $content = self::getJsonBody($content); - [$key, $value] = self::splitJoltSingleton($content); - - switch ($key) { - case 'header': - if (isset($data->header)) { - throw new UnexpectedValueException('Jolt response with second header before summary received'); - } - $data->header = $value; - break; - case 'data': - if (!isset($data->header)) { - throw new UnexpectedValueException('Jolt response with data before new header received'); - } - $data->data[] = $value; - break; - case 'summary': - if (!isset($data->header)) { - throw new UnexpectedValueException('Jolt response with summary before new header received'); - } - $data->summary = $value; - $rtr->results[] = $data; - $data = new stdClass(); - $data->data = []; - break; - - case 'info': - if (isset($rtr->info)) { - throw new UnexpectedValueException('Jolt response with multiple info rows received'); - } - $rtr->info = $value; - break; - case 'error': - if (isset($rtr->errors)) { - throw new UnexpectedValueException('Jolt response with multiple error rows received'); - } - $rtr->errors = []; - foreach ($value->errors as $error) { - $rtr->errors[] = (object) [ - 'code' => self::splitJoltSingleton($error->code)[1], - 'message' => self::splitJoltSingleton($error->message)[1], - ]; - } - break; - default: - throw new UnexpectedValueException('Jolt response with unknown key received: '.$key); - } - } - - return $rtr; - } - - /** - * @pure - * - * @return array{0: string, 1: mixed} - */ - public static function splitJoltSingleton(stdClass $joltSingleton): array - { - /** @var array $joltSingleton */ - $joltSingleton = (array) $joltSingleton; - - if (count($joltSingleton) !== 1) { - throw new UnexpectedValueException('stdClass with '.count($joltSingleton).' elements is not a Jolt singleton.'); - } - - $key = array_key_first($joltSingleton); - - return [$key, $joltSingleton[$key]]; - } - - /** - * Prepares the statements to json. - * - * @param iterable $statements - * - * @throws JsonException - */ - public static function statementsToJson(ConnectionInterface $connection, FormatterInterface $formatter, iterable $statements): string - { - $tbr = []; - foreach ($statements as $statement) { - $st = [ - 'statement' => $statement->getText(), - 'resultDataContents' => [], - 'includeStats' => false, - ]; - $st = array_merge($st, $formatter->statementConfigOverride($connection)); - $parameters = ParameterHelper::formatParameters($statement->getParameters(), $connection->getProtocol()); - $st['parameters'] = $parameters->count() === 0 ? new stdClass() : $parameters->toArray(); - $tbr[] = $st; - } - - return json_encode([ - 'statements' => $tbr, - ], JSON_THROW_ON_ERROR); - } -} diff --git a/src/Http/HttpSession.php b/src/Http/HttpSession.php deleted file mode 100644 index 280e9b84..00000000 --- a/src/Http/HttpSession.php +++ /dev/null @@ -1,191 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use JsonException; -use Laudis\Neo4j\Common\GeneratorHelper; -use Laudis\Neo4j\Common\Resolvable; -use Laudis\Neo4j\Common\TransactionHelper; -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; -use Laudis\Neo4j\Contracts\SessionInterface; -use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; -use Laudis\Neo4j\Databags\Bookmark; -use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Databags\TransactionConfiguration; -use Laudis\Neo4j\Enum\AccessMode; -use Laudis\Neo4j\Types\CypherList; - -use function microtime; -use function parse_url; - -use const PHP_URL_PATH; - -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\StreamFactoryInterface; -use stdClass; - -/** - * @template T - * - * @implements SessionInterface - */ -final class HttpSession implements SessionInterface -{ - /** - * @psalm-mutation-free - * - * @param Resolvable $streamFactory - * @param FormatterInterface $formatter - * @param Resolvable $requestFactory - * @param Resolvable $uri - */ - public function __construct( - /** - * @psalm-readonly - */ - private readonly Resolvable $streamFactory, - /** @psalm-readonly */ - private readonly HttpConnectionPool $pool, - /** @psalm-readonly */ - private readonly SessionConfiguration $config, - /** - * @psalm-readonly - */ - private readonly FormatterInterface $formatter, - /** - * @psalm-readonly - */ - private readonly Resolvable $requestFactory, - /** - * @psalm-readonly - */ - private readonly Resolvable $uri, - AuthenticateInterface $auth, - string $userAgent - ) {} - - /** - * @throws JsonException - */ - public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList - { - $request = $this->requestFactory->resolve()->createRequest('POST', $this->uri->resolve()); - $connection = $this->pool->acquire($this->config); - /** @var HttpConnection */ - $connection = GeneratorHelper::getReturnFromGenerator($connection); - $content = HttpHelper::statementsToJson($connection, $this->formatter, $statements); - $request = $this->formatter->decorateRequest($request, $connection); - $request = $this->instantCommitRequest($request)->withBody($this->streamFactory->resolve()->createStream($content)); - - $start = microtime(true); - $response = $connection->getImplementation()->sendRequest($request); - $time = microtime(true) - $start; - - $data = HttpHelper::interpretResponse($response); - - return $this->formatter->formatHttpResult($response, $data, $connection, $time, $time, $statements); - } - - public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) - { - return TransactionHelper::retry(fn () => $this->beginTransaction(), $tsxHandler); - } - - public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) - { - return $this->writeTransaction($tsxHandler, $config); - } - - public function transaction(callable $tsxHandler, ?TransactionConfiguration $config = null) - { - if ($this->config->getAccessMode() === AccessMode::WRITE()) { - return $this->writeTransaction($tsxHandler, $config); - } - - return $this->readTransaction($tsxHandler, $config); - } - - /** - * @throws JsonException - */ - public function runStatement(Statement $statement, ?TransactionConfiguration $config = null) - { - return $this->runStatements([$statement], $config)->first(); - } - - /** - * @throws JsonException - */ - public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null) - { - return $this->runStatement(Statement::create($statement, $parameters), $config); - } - - /** - * @throws JsonException - */ - public function beginTransaction(?iterable $statements = null, ?TransactionConfiguration $config = null): UnmanagedTransactionInterface - { - $request = $this->requestFactory->resolve()->createRequest('POST', $this->uri->resolve()); - $connection = $this->pool->acquire($this->config); - /** @var HttpConnection */ - $connection = GeneratorHelper::getReturnFromGenerator($connection); - - $request = $this->formatter->decorateRequest($request, $connection); - $request->getBody()->write(HttpHelper::statementsToJson($connection, $this->formatter, $statements ?? [])); - $response = $connection->getImplementation()->sendRequest($request); - - $response = HttpHelper::interpretResponse($response); - if (isset($response->info) && $response->info instanceof stdClass) { - /** @var string */ - $url = $response->info->commit; - } else { - /** @var string */ - $url = $response->commit; - } - $path = str_replace('/commit', '', parse_url($url, PHP_URL_PATH)); - $uri = $request->getUri()->withPath($path); - $request = $request->withUri($uri); - - return $this->makeTransaction($connection, $request); - } - - /** - * @return HttpUnmanagedTransaction - */ - private function makeTransaction(HttpConnection $connection, RequestInterface $request): HttpUnmanagedTransaction - { - return new HttpUnmanagedTransaction( - $request, - $connection, - $this->streamFactory->resolve(), - $this->formatter - ); - } - - private function instantCommitRequest(RequestInterface $request): RequestInterface - { - $path = $request->getUri()->getPath().'/commit'; - $uri = $request->getUri()->withPath($path); - - return $request->withUri($uri); - } - - public function getLastBookmark(): Bookmark - { - return new Bookmark([]); - } -} diff --git a/src/Http/HttpUnmanagedTransaction.php b/src/Http/HttpUnmanagedTransaction.php deleted file mode 100644 index 7de93457..00000000 --- a/src/Http/HttpUnmanagedTransaction.php +++ /dev/null @@ -1,171 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use function array_intersect; -use function array_unique; - -use Laudis\Neo4j\Common\TransactionHelper; -use Laudis\Neo4j\Contracts\FormatterInterface; -use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; -use Laudis\Neo4j\Databags\Neo4jError; -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Exception\Neo4jException; -use Laudis\Neo4j\Types\CypherList; - -use function microtime; - -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\StreamFactoryInterface; -use stdClass; - -/** - * @template T - * - * @implements UnmanagedTransactionInterface - */ -final class HttpUnmanagedTransaction implements UnmanagedTransactionInterface -{ - private bool $isCommitted = false; - - private bool $isRolledBack = false; - - /** - * @psalm-mutation-free - * - * @param FormatterInterface $formatter - */ - public function __construct( - /** @psalm-readonly */ - private readonly RequestInterface $request, - /** @psalm-readonly */ - private readonly HttpConnection $connection, - /** @psalm-readonly */ - private readonly StreamFactoryInterface $factory, - /** - * @psalm-readonly - */ - private readonly FormatterInterface $formatter - ) {} - - public function run(string $statement, iterable $parameters = []) - { - return $this->runStatement(new Statement($statement, $parameters)); - } - - public function runStatement(Statement $statement) - { - return $this->runStatements([$statement])->first(); - } - - public function runStatements(iterable $statements): CypherList - { - $request = $this->request->withMethod('POST'); - - $body = HttpHelper::statementsToJson($this->connection, $this->formatter, $statements); - - $request = $request->withBody($this->factory->createStream($body)); - $start = microtime(true); - $response = $this->connection->getImplementation()->sendRequest($request); - $total = microtime(true) - $start; - - $data = $this->handleResponse($response); - - return $this->formatter->formatHttpResult($response, $data, $this->connection, $total, $total, $statements); - } - - public function commit(iterable $statements = []): CypherList - { - $uri = $this->request->getUri(); - $request = $this->request->withUri($uri->withPath($uri->getPath().'/commit'))->withMethod('POST'); - - $content = HttpHelper::statementsToJson($this->connection, $this->formatter, $statements); - $request = $request->withBody($this->factory->createStream($content)); - - $start = microtime(true); - $response = $this->connection->getImplementation()->sendRequest($request); - $total = microtime(true) - $start; - - $data = $this->handleResponse($response); - - $this->isCommitted = true; - - return $this->formatter->formatHttpResult($response, $data, $this->connection, $total, $total, $statements); - } - - public function rollback(): void - { - $request = $this->request->withMethod('DELETE'); - $response = $this->connection->getImplementation()->sendRequest($request); - - $this->handleResponse($response); - - $this->isRolledBack = true; - } - - public function __destruct() - { - $this->connection->close(); - } - - public function isRolledBack(): bool - { - return $this->isRolledBack; - } - - public function isCommitted(): bool - { - return $this->isCommitted; - } - - public function isFinished(): bool - { - return $this->isRolledBack() || $this->isCommitted(); - } - - /** - * @throws Neo4jException - * - * @return never - */ - private function handleNeo4jException(Neo4jException $e): void - { - if (!$this->isFinished()) { - $classifications = array_map(static fn (Neo4jError $e) => $e->getClassification(), $e->getErrors()); - $classifications = array_unique($classifications); - - $intersection = array_intersect($classifications, TransactionHelper::ROLLBACK_CLASSIFICATIONS); - if ($intersection !== []) { - $this->isRolledBack = true; - } - } - - throw $e; - } - - /** - * @throws Neo4jException - */ - private function handleResponse(ResponseInterface $response): stdClass - { - try { - $data = HttpHelper::interpretResponse($response); - } catch (Neo4jException $e) { - $this->handleNeo4jException($e); - } - - return $data; - } -} diff --git a/src/Http/RequestFactory.php b/src/Http/RequestFactory.php deleted file mode 100644 index a8b04eb5..00000000 --- a/src/Http/RequestFactory.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Http; - -use Laudis\Neo4j\Contracts\AuthenticateInterface; -use Psr\Http\Message\RequestFactoryInterface; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\UriInterface; - -/** - * Request factory decorator to correctly configure a default Request. - */ -final class RequestFactory implements RequestFactoryInterface -{ - /** - * @psalm-mutation-free - */ - public function __construct( - /** @readonly */ - private readonly RequestFactoryInterface $requestFactory, - /** @readonly */ - private readonly AuthenticateInterface $authenticate, - /** @readonly */ - private readonly UriInterface $authUri, - /** @readonly */ - private readonly string $userAgent - ) {} - - public function createRequest(string $method, $uri): RequestInterface - { - $request = $this->requestFactory->createRequest($method, $uri); - $request = $this->authenticate->authenticateHttp($request, $this->authUri, $this->userAgent); - $uri = $request->getUri()->withUserInfo(''); - $port = $uri->getPort(); - if ($port === null) { - $port = $uri->getScheme() === 'https' ? 7473 : 7474; - $uri = $uri->withPort($port); - } - - return $request - ->withUri($uri) - ->withHeader('Accept', 'application/json;charset=UTF-8') - ->withHeader('Content-Type', 'application/json'); - } -} diff --git a/src/ParameterHelper.php b/src/ParameterHelper.php index d4667411..3d7c20fa 100644 --- a/src/ParameterHelper.php +++ b/src/ParameterHelper.php @@ -85,7 +85,7 @@ public static function asParameter( ): iterable|int|float|bool|string|stdClass|IStructure|null { return self::cypherMapToStdClass($value) ?? self::emptySequenceToArray($value) ?? - self::convertBoltConvertibles($value, $protocol) ?? + self::convertBoltConvertibles($value) ?? self::convertTemporalTypes($value, $protocol) ?? self::filledIterableToArray($value, $protocol) ?? self::stringAbleToString($value) ?? @@ -193,7 +193,9 @@ private static function iterableToArray(iterable $value, ConnectionProtocol $pro if (is_int($key) || is_string($key)) { $tbr[$key] = self::asParameter($val, $protocol); } else { - $msg = 'Iterable parameters must have an integer or string as key values, '.gettype($key).' received.'; + $msg = 'Iterable parameters must have an integer or string as key values, '.gettype( + $key + ).' received.'; throw new InvalidArgumentException($msg); } } @@ -201,9 +203,9 @@ private static function iterableToArray(iterable $value, ConnectionProtocol $pro return $tbr; } - private static function convertBoltConvertibles(mixed $value, ConnectionProtocol $protocol): ?IStructure + private static function convertBoltConvertibles(mixed $value): ?IStructure { - if ($protocol->isBolt() && $value instanceof BoltConvertibleInterface) { + if ($value instanceof BoltConvertibleInterface) { return $value->convertToBolt(); } @@ -212,31 +214,29 @@ private static function convertBoltConvertibles(mixed $value, ConnectionProtocol private static function convertTemporalTypes(mixed $value, ConnectionProtocol $protocol): ?IStructure { - if ($protocol->isBolt()) { - if ($value instanceof DateTimeInterface) { - if ($protocol->compare(ConnectionProtocol::BOLT_V44()) > 0) { - return new \Bolt\protocol\v5\structures\DateTimeZoneId( - $value->getTimestamp(), - ((int) $value->format('u')) * 1000, - $value->getTimezone()->getName() - ); - } - - return new DateTimeZoneId( + if ($value instanceof DateTimeInterface) { + if ($protocol->compare(ConnectionProtocol::BOLT_V44()) > 0) { + return new \Bolt\protocol\v5\structures\DateTimeZoneId( $value->getTimestamp(), ((int) $value->format('u')) * 1000, $value->getTimezone()->getName() ); } - if ($value instanceof DateInterval) { - return new Duration( - $value->y * 12 + $value->m, - $value->d, - $value->h * 60 * 60 * $value->i * 60 + $value->s * 60, - (int) ($value->f * 1000) - ); - } + return new DateTimeZoneId( + $value->getTimestamp(), + ((int) $value->format('u')) * 1000, + $value->getTimezone()->getName() + ); + } + + if ($value instanceof DateInterval) { + return new Duration( + $value->y * 12 + $value->m, + $value->d, + $value->h * 60 * 60 * $value->i * 60 + $value->s * 60, + (int) ($value->f * 1000) + ); } return null; diff --git a/tests/Integration/ClientIntegrationTest.php b/tests/Integration/ClientIntegrationTest.php index 1e1e072a..a02e8e87 100644 --- a/tests/Integration/ClientIntegrationTest.php +++ b/tests/Integration/ClientIntegrationTest.php @@ -276,7 +276,6 @@ public function testInvalidConnectionCheck(): void $client = ClientBuilder::create() ->withDriver('bolt', 'bolt://localboast') ->withDriver('neo4j', 'neo4j://localboast') - ->withDriver('http', 'http://localboast') ->build(); $exceptionThrownCount = 0; @@ -292,14 +291,8 @@ public function testInvalidConnectionCheck(): void self::assertInstanceOf(RuntimeException::class, $e); ++$exceptionThrownCount; } - try { - $client->verifyConnectivity('http'); - } catch (Exception $e) { - self::assertInstanceOf(RuntimeException::class, $e); - ++$exceptionThrownCount; - } - self::assertEquals(3, $exceptionThrownCount); + self::assertEquals(2, $exceptionThrownCount); } public function testValidConnectionCheck(): void From 00d950e86a54d008875598297270a1b84e6dab05 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Fri, 24 Jan 2025 14:32:08 +0530 Subject: [PATCH 02/10] WIP --- README.md | 15 +- src/Basic/UnmanagedTransaction.php | 9 +- src/Bolt/BoltConnection.php | 4 +- src/Bolt/BoltUnmanagedTransaction.php | 9 +- src/Bolt/Session.php | 16 +- src/Common/DriverSetupManager.php | 8 +- src/Contracts/ClientInterface.php | 26 +- src/Contracts/FormatterInterface.php | 117 ----- src/Contracts/SessionInterface.php | 24 +- src/Contracts/TransactionInterface.php | 14 +- .../UnmanagedTransactionInterface.php | 6 +- src/Databags/SummarizedResult.php | 12 +- src/DriverFactory.php | 4 +- src/Formatter/OGMFormatter.php | 175 ------- .../Specialised/BoltOGMTranslator.php | 4 +- src/Formatter/SummarizedResultFormatter.php | 195 ++++---- src/Http/HttpHelper.php | 12 +- src/Http/HttpUnmanagedTransaction.php | 11 +- src/Neo4j/Neo4jDriver.php | 15 +- src/Types/AbstractPropertyObject.php | 3 +- src/Types/CypherMap.php | 4 +- src/Types/Node.php | 3 +- src/Types/Relationship.php | 4 +- src/Types/UnboundRelationship.php | 3 +- .../Integration/BoltDriverIntegrationTest.php | 3 - tests/Integration/ClientIntegrationTest.php | 9 +- tests/Integration/ComplexQueryTest.php | 5 +- .../OGMFormatterIntegrationTest.php | 451 ------------------ 28 files changed, 169 insertions(+), 992 deletions(-) delete mode 100644 src/Contracts/FormatterInterface.php delete mode 100644 src/Formatter/OGMFormatter.php delete mode 100644 tests/Integration/OGMFormatterIntegrationTest.php diff --git a/README.md b/README.md index 020a749c..48f55aed 100644 --- a/README.md +++ b/README.md @@ -308,21 +308,16 @@ composer require nyholm/psr7 nyholm/psr7-server kriswallsmith/buzz ## Result formats/hydration -In order to make the results of the bolt protocol and the http uniform, the driver provides result formatters (aka hydrators). The client is configurable with these formatters. You can even implement your own. +In order to make the results of the bolt protocol and the http uniform, the driver provides a summarizes the results. -The default formatter is the `\Laudis\Neo4j\Formatters\OGMFormatter`, which is explained extensively in [the result format section](#accessing-the-results). +The default formatter is the `\Laudis\Neo4j\Formatters\SummarizedResultFormatter`, which is explained extensively in [the result format section](#accessing-the-results). -The driver provides three formatters by default, which are all found in the Formatter namespace: - - `\Laudis\Neo4j\Formatter\BasicFormatter` which erases all the Cypher types and simply returns every value in the resulting map as a [scalar](https://www.php.net/manual/en/function.is-scalar.php), null or array value. - - `\Laudis\Neo4j\Formatter\OGMFormatter` which maps the cypher types to php types as explained [here](#accessing-the-results). - - `\Laudis\Neo4j\Formatter\SummarizedResultFormatter` which decorates any formatter and adds an extensive result summary. +`\Laudis\Neo4j\Formatter\SummarizedResultFormatter` adds an extensive result summary. The client builder provides an easy way to change the formatter: ```php -$client = \Laudis\Neo4j\ClientBuilder::create() - ->withFormatter(\Laudis\Neo4j\Formatter\SummarizedResultFormatter::create()) - ->build(); +$client = \Laudis\Neo4j\ClientBuilder::create()->build(); /** * The client will now return a result, decorated with a summary. @@ -339,8 +334,6 @@ $summary = $summarisedResult->getSummary(); $result = $summarisedResult->getResult(); ``` -In order to use a custom formatter, implement the `Laudis\Neo4j\Contracts\FormatterInterface` and provide it when using the client builder. - ## Concepts The driver API described [here](https://neo4j.com/docs/driver-manual/current/) is the main target of the driver. Because of this, the client is nothing more than a driver manager. The driver creates sessions. A session runs queries through a transaction. diff --git a/src/Basic/UnmanagedTransaction.php b/src/Basic/UnmanagedTransaction.php index 233e582d..beef7e1e 100644 --- a/src/Basic/UnmanagedTransaction.php +++ b/src/Basic/UnmanagedTransaction.php @@ -33,17 +33,12 @@ public function __construct( /** * @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); @@ -52,7 +47,7 @@ public function runStatement(Statement $statement): SummarizedResult /** * @param iterable $statements * - * @return CypherList> + * @return CypherList */ public function runStatements(iterable $statements): CypherList { @@ -62,7 +57,7 @@ public function runStatements(iterable $statements): CypherList /** * @param iterable $statements * - * @return CypherList> + * @return CypherList */ public function commit(iterable $statements = []): CypherList { diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 93f3b3fa..40710426 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -27,13 +27,13 @@ use Laudis\Neo4j\Common\Neo4jLogger; 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\Databags\Neo4jError; use Laudis\Neo4j\Enum\AccessMode; use Laudis\Neo4j\Enum\ConnectionProtocol; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Types\CypherList; use Psr\Http\Message\UriInterface; use Psr\Log\LogLevel; @@ -42,7 +42,7 @@ /** * @implements ConnectionInterface * - * @psalm-import-type BoltMeta from FormatterInterface + * @psalm-import-type BoltMeta from SummarizedResultFormatter */ class BoltConnection implements ConnectionInterface { diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index 9a547551..d8254348 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -14,7 +14,6 @@ namespace Laudis\Neo4j\Bolt; use Bolt\enum\ServerState; -use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\SessionConfiguration; @@ -22,6 +21,7 @@ use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Enum\TransactionState; use Laudis\Neo4j\Exception\ClientException; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\ParameterHelper; use Laudis\Neo4j\Types\AbstractCypherSequence; use Laudis\Neo4j\Types\CypherList; @@ -37,22 +37,19 @@ * * @implements UnmanagedTransactionInterface * - * @psalm-import-type BoltMeta from FormatterInterface + * @psalm-import-type BoltMeta from SummarizedResultFormatter */ final class BoltUnmanagedTransaction implements UnmanagedTransactionInterface { private TransactionState $state = TransactionState::ACTIVE; - /** - * @param FormatterInterface $formatter - */ public function __construct( /** @psalm-readonly */ private readonly ?string $database, /** * @psalm-readonly */ - private readonly FormatterInterface $formatter, + private readonly SummarizedResultFormatter $formatter, /** @psalm-readonly */ private readonly BoltConnection $connection, private readonly SessionConfiguration $config, diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php index 8d895d59..f8042597 100644 --- a/src/Bolt/Session.php +++ b/src/Bolt/Session.php @@ -18,7 +18,6 @@ use Laudis\Neo4j\Common\Neo4jLogger; 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; @@ -26,19 +25,17 @@ 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 Psr\Log\LogLevel; /** * A session using bolt connections. - * - * @template ResultFormat - * - * @implements SessionInterface */ final class Session implements SessionInterface { @@ -47,7 +44,6 @@ final class Session implements SessionInterface /** * @param ConnectionPool|Neo4jConnectionPool $pool - * @param FormatterInterface $formatter * * @psalm-mutation-free */ @@ -58,7 +54,7 @@ public function __construct( /** * @psalm-readonly */ - private readonly FormatterInterface $formatter + private readonly SummarizedResultFormatter $formatter ) { $this->bookmarkHolder = new BookmarkHolder(Bookmark::from($config->getBookmarks())); } @@ -84,12 +80,12 @@ public function openTransaction(?iterable $statements = null, ?TransactionConfig 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); } @@ -133,7 +129,7 @@ public function beginTransaction(?iterable $statements = null, ?TransactionConfi } /** - * @return UnmanagedTransactionInterface + * @return UnmanagedTransactionInterface */ private function beginInstantTransaction( SessionConfiguration $config, diff --git a/src/Common/DriverSetupManager.php b/src/Common/DriverSetupManager.php index bb2639d2..78274b17 100644 --- a/src/Common/DriverSetupManager.php +++ b/src/Common/DriverSetupManager.php @@ -22,11 +22,11 @@ use InvalidArgumentException; use Laudis\Neo4j\Authentication\Authenticate; use Laudis\Neo4j\Contracts\DriverInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\DriverConfiguration; use Laudis\Neo4j\Databags\DriverSetup; use Laudis\Neo4j\Databags\SessionConfiguration; use Laudis\Neo4j\DriverFactory; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use const PHP_INT_MIN; @@ -51,11 +51,9 @@ class DriverSetupManager implements Countable /** * @psalm-mutation-free - * - * @param FormatterInterface $formatter */ public function __construct( - private FormatterInterface $formatter, + private SummarizedResultFormatter $formatter, private DriverConfiguration $configuration ) {} @@ -193,7 +191,7 @@ public function count(): int * * @psalm-mutation-free */ - public function withFormatter(FormatterInterface $formatter): self + public function withFormatter(SummarizedResultFormatter $formatter): self { $tbr = clone $this; $tbr->formatter = $formatter; diff --git a/src/Contracts/ClientInterface.php b/src/Contracts/ClientInterface.php index 5a5f1d4c..e07a7ec4 100644 --- a/src/Contracts/ClientInterface.php +++ b/src/Contracts/ClientInterface.php @@ -14,15 +14,11 @@ namespace Laudis\Neo4j\Contracts; 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; -/** - * @template ResultFormat - * - * @extends TransactionInterface - */ interface ClientInterface extends TransactionInterface { /** @@ -31,19 +27,15 @@ interface ClientInterface extends TransactionInterface * @param iterable $parameters * * @throws Neo4jException - * - * @return ResultFormat */ - public function run(string $statement, iterable $parameters = [], ?string $alias = null); + public function run(string $statement, iterable $parameters = [], ?string $alias = null): SummarizedResult; /** * 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); + public function runStatement(Statement $statement, ?string $alias = null): SummarizedResult; /** * Runs a one off transaction with the provided statements over the connection with the provided alias or the master alias otherwise. @@ -52,7 +44,7 @@ public function runStatement(Statement $statement, ?string $alias = null); * * @throws Neo4jException * - * @return CypherList + * @return CypherList */ public function runStatements(iterable $statements, ?string $alias = null): CypherList; @@ -62,8 +54,6 @@ public function runStatements(iterable $statements, ?string $alias = null): Cyph * @param iterable|null $statements * * @throws Neo4jException - * - * @return UnmanagedTransactionInterface */ public function beginTransaction(?iterable $statements = null, ?string $alias = null, ?TransactionConfiguration $config = null): UnmanagedTransactionInterface; @@ -71,8 +61,6 @@ public function beginTransaction(?iterable $statements = null, ?string $alias = * 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; @@ -84,7 +72,7 @@ public function hasDriver(string $alias): bool; /** * @template U * - * @param callable(TransactionInterface):U $tsxHandler + * @param callable(TransactionInterface):U $tsxHandler * * @return U */ @@ -93,7 +81,7 @@ public function writeTransaction(callable $tsxHandler, ?string $alias = null, ?T /** * @template U * - * @param callable(TransactionInterface):U $tsxHandler + * @param callable(TransactionInterface):U $tsxHandler * * @return U */ @@ -104,7 +92,7 @@ public function readTransaction(callable $tsxHandler, ?string $alias = null, ?Tr * * @template U * - * @param callable(TransactionInterface):U $tsxHandler + * @param callable(TransactionInterface):U $tsxHandler * * @return U */ diff --git a/src/Contracts/FormatterInterface.php b/src/Contracts/FormatterInterface.php deleted file mode 100644 index ded59d2e..00000000 --- a/src/Contracts/FormatterInterface.php +++ /dev/null @@ -1,117 +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, - * db?: string - * } - * @psalm-type CypherError = array{code: string, message: string} - * @psalm-type CypherRowResponse = array{row: list>} - * @psalm-type CypherResponse = array{columns:list, data:list, stats?:CypherStats} - * @psalm-type CypherResponseSet = array{results: list, errors: list} - * @psalm-type BoltMeta = array{t_first: int, fields: list, qid ?: int} - * - * @template ResultFormat - * - * @deprecated Next major version will only use SummarizedResultFormatter - */ -interface FormatterInterface -{ - /** - * Formats the results of the bolt protocol to the unified format. - * - * @param BoltMeta $meta - * - * @return ResultFormat - */ - public function formatBoltResult(array $meta, BoltResult $result, BoltConnection $connection, float $runStart, float $resultAvailableAfter, Statement $statement, BookmarkHolder $holder); - - /** - * Formats the results of the HTTP protocol to the unified format. - * - * @param iterable $statements - * - * @throws JsonException - * - * @return CypherList - * - * @psalm-mutation-free - */ - public function formatHttpResult(ResponseInterface $response, stdClass $body, HttpConnection $connection, float $resultsAvailableAfter, float $resultsConsumedAfter, iterable $statements): CypherList; - - /** - * Decorates a request to make make sure it requests the correct format. - * - * @see https://neo4j.com/docs/http-api/current/actions/result-format/ - * - * @psalm-mutation-free - */ - public function decorateRequest(RequestInterface $request, ConnectionInterface $connection): RequestInterface; - - /** - * Overrides the statement config of the HTTP protocol. - * - * @see https://neo4j.com/docs/http-api/current/actions/result-format/ - * - * @return array{resultDataContents?: list<'GRAPH'|'ROW'|'REST'>, includeStats?:bool} - * - * @psalm-mutation-free - */ - public function statementConfigOverride(ConnectionInterface $connection): array; -} diff --git a/src/Contracts/SessionInterface.php b/src/Contracts/SessionInterface.php index 57eeeb5b..b7b39e61 100644 --- a/src/Contracts/SessionInterface.php +++ b/src/Contracts/SessionInterface.php @@ -15,16 +15,13 @@ 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; /** * A lightweight container for causally chained sequences of transactions to carry out work. - * - * @template ResultFormat - * - * @extends TransactionInterface */ interface SessionInterface extends TransactionInterface { @@ -33,35 +30,28 @@ interface SessionInterface extends TransactionInterface * * @throws Neo4jException * - * @return CypherList + * @return CypherList */ public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList; - /** - * @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 */ public function beginTransaction(?iterable $statements = null, ?TransactionConfiguration $config = null): UnmanagedTransactionInterface; /** * @template HandlerResult * - * @param callable(TransactionInterface):HandlerResult $tsxHandler + * @param callable(TransactionInterface):HandlerResult $tsxHandler * * @return HandlerResult */ @@ -70,7 +60,7 @@ public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration /** * @template HandlerResult * - * @param callable(TransactionInterface):HandlerResult $tsxHandler + * @param callable(TransactionInterface):HandlerResult $tsxHandler * * @return HandlerResult */ @@ -79,7 +69,7 @@ public function readTransaction(callable $tsxHandler, ?TransactionConfiguration /** * @template HandlerResult * - * @param callable(TransactionInterface):HandlerResult $tsxHandler + * @param callable(TransactionInterface):HandlerResult $tsxHandler * * @return HandlerResult */ diff --git a/src/Contracts/TransactionInterface.php b/src/Contracts/TransactionInterface.php index d4c77ed0..329af20d 100644 --- a/src/Contracts/TransactionInterface.php +++ b/src/Contracts/TransactionInterface.php @@ -14,36 +14,30 @@ namespace Laudis\Neo4j\Contracts; use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Exception\Neo4jException; use Laudis\Neo4j\Types\CypherList; /** * Transactions are atomic units of work that may contain one or more query. * - * @template ResultFormat - * * @see https://neo4j.com/docs/cypher-manual/current/introduction/transactions/ */ interface TransactionInterface { /** * @param iterable $parameters - * - * @return ResultFormat */ - public function run(string $statement, iterable $parameters = []); + public function run(string $statement, iterable $parameters = []): SummarizedResult; - /** - * @return ResultFormat - */ - public function runStatement(Statement $statement); + public function runStatement(Statement $statement): SummarizedResult; /** * @param iterable $statements * * @throws Neo4jException * - * @return CypherList + * @return CypherList */ public function runStatements(iterable $statements): CypherList; } diff --git a/src/Contracts/UnmanagedTransactionInterface.php b/src/Contracts/UnmanagedTransactionInterface.php index 25f19e65..09dabaa7 100644 --- a/src/Contracts/UnmanagedTransactionInterface.php +++ b/src/Contracts/UnmanagedTransactionInterface.php @@ -19,10 +19,6 @@ /** * 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,7 +28,7 @@ interface UnmanagedTransactionInterface extends TransactionInterface * * @param iterable $statements * - * @return CypherList + * @return CypherList */ public function commit(iterable $statements = []): CypherList; diff --git a/src/Databags/SummarizedResult.php b/src/Databags/SummarizedResult.php index e72249a2..846966eb 100644 --- a/src/Databags/SummarizedResult.php +++ b/src/Databags/SummarizedResult.php @@ -20,20 +20,18 @@ /** * A result containing the values and the summary. * - * @template TValue - * - * @extends CypherList + * @extends CypherList */ final class SummarizedResult extends CypherList { private ?ResultSummary $summary = null; /** - * @param iterable|callable():Generator $iterable + * @param iterable|callable():Generator $iterable * * @psalm-mutation-free */ - public function __construct(?ResultSummary &$summary, $iterable = []) + public function __construct(?ResultSummary &$summary, iterable|callable $iterable = []) { parent::__construct($iterable); $this->summary = &$summary; @@ -42,9 +40,9 @@ public function __construct(?ResultSummary &$summary, $iterable = []) /** * @template Value * - * @param callable():(\Generator) $operation + * @param callable():(Generator) $operation * - * @return static + * @return static * * @psalm-mutation-free */ diff --git a/src/DriverFactory.php b/src/DriverFactory.php index 85d7f3b4..e5d04c32 100644 --- a/src/DriverFactory.php +++ b/src/DriverFactory.php @@ -22,7 +22,7 @@ use Laudis\Neo4j\Contracts\DriverInterface; use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\DriverConfiguration; -use Laudis\Neo4j\Formatter\OGMFormatter; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Http\HttpDriver; use Laudis\Neo4j\Neo4j\Neo4jDriver; use Psr\Http\Message\UriInterface; @@ -30,7 +30,7 @@ /** * Factory for creating drivers directly. * - * @psalm-import-type OGMResults from OGMFormatter + * @psalm-import-type OGMResults from SummarizedResultFormatter */ final class DriverFactory { diff --git a/src/Formatter/OGMFormatter.php b/src/Formatter/OGMFormatter.php deleted file mode 100644 index b2a03e08..00000000 --- a/src/Formatter/OGMFormatter.php +++ /dev/null @@ -1,175 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Formatter; - -use function array_key_exists; - -use Laudis\Neo4j\Bolt\BoltConnection; -use Laudis\Neo4j\Bolt\BoltResult; -use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; -use Laudis\Neo4j\Databags\Bookmark; -use Laudis\Neo4j\Databags\BookmarkHolder; -use Laudis\Neo4j\Databags\Statement; -use Laudis\Neo4j\Formatter\Specialised\BoltOGMTranslator; -use Laudis\Neo4j\Formatter\Specialised\JoltHttpOGMTranslator; -use Laudis\Neo4j\Formatter\Specialised\LegacyHttpOGMTranslator; -use Laudis\Neo4j\Types\Cartesian3DPoint; -use Laudis\Neo4j\Types\CartesianPoint; -use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; -use Laudis\Neo4j\Types\Date; -use Laudis\Neo4j\Types\DateTime; -use Laudis\Neo4j\Types\DateTimeZoneId; -use Laudis\Neo4j\Types\Duration; -use Laudis\Neo4j\Types\LocalDateTime; -use Laudis\Neo4j\Types\LocalTime; -use Laudis\Neo4j\Types\Node; -use Laudis\Neo4j\Types\Path; -use Laudis\Neo4j\Types\Relationship; -use Laudis\Neo4j\Types\Time; -use Laudis\Neo4j\Types\WGS843DPoint; -use Laudis\Neo4j\Types\WGS84Point; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; -use stdClass; - -use function version_compare; - -/** - * Formats the result in a basic OGM (Object Graph Mapping) format by mapping al cypher types to types found in the \Laudis\Neo4j\Types namespace. - * - * @see https://neo4j.com/docs/driver-manual/current/cypher-workflow/#driver-type-mapping - * - * @psalm-type OGMTypes = string|int|float|bool|null|Date|DateTime|Duration|LocalDateTime|LocalTime|Time|Node|Relationship|Path|Cartesian3DPoint|CartesianPoint|WGS84Point|WGS843DPoint|DateTimeZoneId|CypherList|CypherMap - * @psalm-type OGMResults = CypherList> - * - * @psalm-import-type BoltMeta from FormatterInterface - * - * @implements FormatterInterface>> - * - * @deprecated Next major version will only use SummarizedResultFormatter - */ -final class OGMFormatter implements FormatterInterface -{ - /** - * @psalm-mutation-free - */ - public function __construct( - private readonly BoltOGMTranslator $boltTranslator, - private readonly JoltHttpOGMTranslator $joltTranslator, - private readonly LegacyHttpOGMTranslator $legacyHttpTranslator - ) {} - - /** - * Creates a new instance of itself. - * - * @pure - */ - public static function create(): OGMFormatter - { - return new self(new BoltOGMTranslator(), new JoltHttpOGMTranslator(), new LegacyHttpOGMTranslator()); - } - - /** - * @param BoltMeta $meta - * - * @return CypherList> - */ - public function formatBoltResult(array $meta, BoltResult $result, BoltConnection $connection, float $runStart, float $resultAvailableAfter, Statement $statement, BookmarkHolder $holder): CypherList - { - $tbr = (new CypherList(function () use ($result, $meta) { - foreach ($result as $row) { - yield $this->formatRow($meta, $row); - } - }))->withCacheLimit($result->getFetchSize()); - - $connection->subscribeResult($tbr); - $result->addFinishedCallback(function (array $response) use ($holder) { - if (array_key_exists('bookmark', $response) && is_string($response['bookmark'])) { - $holder->setBookmark(new Bookmark([$response['bookmark']])); - } - }); - - return $tbr; - } - - /** - * @psalm-mutation-free - */ - public function formatHttpResult( - ResponseInterface $response, - stdClass $body, - ConnectionInterface $connection, - float $resultsAvailableAfter, - float $resultsConsumedAfter, - iterable $statements - ): CypherList { - return $this->decideTranslator($connection)->formatHttpResult( - $response, - $body, - $connection, - $resultsAvailableAfter, - $resultsConsumedAfter, - $statements - ); - } - - /** - * @psalm-mutation-free - */ - private function decideTranslator(ConnectionInterface $connection): LegacyHttpOGMTranslator|JoltHttpOGMTranslator - { - if (version_compare($connection->getServerAgent(), '4.2.5') <= 0) { - return $this->legacyHttpTranslator; - } - - return $this->joltTranslator; - } - - /** - * @param BoltMeta $meta - * @param list $result - * - * @return CypherMap - * - * @psalm-mutation-free - */ - private function formatRow(array $meta, array $result): CypherMap - { - /** @var array $map */ - $map = []; - foreach ($meta['fields'] as $i => $column) { - $map[$column] = $this->boltTranslator->mapValueToType($result[$i]); - } - - return new CypherMap($map); - } - - /** - * @psalm-mutation-free - */ - public function decorateRequest(RequestInterface $request, ConnectionInterface $connection): RequestInterface - { - return $this->decideTranslator($connection)->decorateRequest($request); - } - - /** - * @psalm-mutation-free - */ - public function statementConfigOverride(ConnectionInterface $connection): array - { - return $this->decideTranslator($connection)->statementConfigOverride(); - } -} diff --git a/src/Formatter/Specialised/BoltOGMTranslator.php b/src/Formatter/Specialised/BoltOGMTranslator.php index bdeb5ecd..43213190 100644 --- a/src/Formatter/Specialised/BoltOGMTranslator.php +++ b/src/Formatter/Specialised/BoltOGMTranslator.php @@ -26,7 +26,7 @@ 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\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Types\Abstract3DPoint; use Laudis\Neo4j\Types\AbstractPoint; use Laudis\Neo4j\Types\Cartesian3DPoint; @@ -51,7 +51,7 @@ /** * Translates Bolt objects to Driver Types. * - * @psalm-import-type OGMTypes from OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter * * @psalm-immutable * diff --git a/src/Formatter/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php index c8864f76..42294913 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -18,8 +18,7 @@ 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\DatabaseInfo; use Laudis\Neo4j\Databags\ResultSummary; @@ -28,106 +27,87 @@ use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\SummaryCounters; use Laudis\Neo4j\Enum\QueryTypeEnum; -use Laudis\Neo4j\Http\HttpConnection; +use Laudis\Neo4j\Formatter\Specialised\BoltOGMTranslator; +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 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>> + * @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-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, + * db?: string + * } + * @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} */ -final class SummarizedResultFormatter implements FormatterInterface +final class SummarizedResultFormatter { /** * @pure */ public static function create(): self { - return new self(OGMFormatter::create()); + return new self(new BoltOGMTranslator()); } /** * @psalm-mutation-free */ public function __construct( - private readonly OGMFormatter $formatter + private readonly BoltOGMTranslator $translator ) {} - /** - * @param CypherList> $results - * - * @return SummarizedResult> - * - * @psalm-mutation-free - */ - public function formatHttpStats(stdClass $response, HttpConnection $connection, Statement $statement, float $resultAvailableAfter, float $resultConsumedAfter, CypherList $results): SummarizedResult - { - if (isset($response->summary) && $response->summary instanceof stdClass) { - /** @var stdClass $stats */ - $stats = $response->summary->stats; - } elseif (isset($response->stats)) { - /** @var stdClass $stats */ - $stats = $response->stats; - } else { - throw new UnexpectedValueException('No stats found in the response set'); - } - - /** - * @psalm-suppress MixedPropertyFetch - * @psalm-suppress MixedArgument - */ - $counters = new SummaryCounters( - $stats->nodes_created ?? 0, - $stats->nodes_deleted ?? 0, - $stats->relationships_created ?? 0, - $stats->relationships_deleted ?? 0, - $stats->properties_set ?? 0, - $stats->labels_added ?? 0, - $stats->labels_removed ?? 0, - $stats->indexes_added ?? 0, - $stats->indexes_removed ?? 0, - $stats->constraints_added ?? 0, - $stats->constraints_removed ?? 0, - $stats->contains_updates ?? false, - $stats->contains_system_updates ?? false, - $stats->system_updates ?? 0, - ); - - $summary = new ResultSummary( - $counters, - $connection->getDatabaseInfo(), - new CypherList(), - null, - null, - $statement, - QueryTypeEnum::fromCounters($counters), - $resultAvailableAfter, - $resultConsumedAfter, - new ServerInfo( - $connection->getServerAddress(), - $connection->getProtocol(), - $connection->getServerAgent() - ) - ); - - /** @var SummarizedResult> */ - return new SummarizedResult($summary, $results); - } - /** * @param array{stats?: BoltCypherStats}&array $response * @@ -170,7 +150,6 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection /** @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; $db = $response['db'] ?? ''; @@ -192,7 +171,7 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection ); }); - $formattedResult = $this->formatter->formatBoltResult($meta, $result, $connection, $runStart, $resultAvailableAfter, $statement, $holder); + $formattedResult = $this->processBoltResult($meta, $result, $connection, $runStart, $resultAvailableAfter, $statement, $holder); /** * @psalm-suppress MixedArgument @@ -203,43 +182,43 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection } /** - * @psalm-mutation-free + * @param BoltMeta $meta * - * @psalm-suppress ImpureMethodCall + * @return CypherList> */ - public function formatHttpResult(ResponseInterface $response, stdClass $body, HttpConnection $connection, float $resultsAvailableAfter, float $resultsConsumedAfter, iterable $statements): CypherList + private function processBoltResult(array $meta, BoltResult $result, BoltConnection $connection, float $runStart, float $resultAvailableAfter, Statement $statement, BookmarkHolder $holder): 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; - } + $tbr = (new CypherList(function () use ($result, $meta) { + foreach ($result as $row) { + yield $this->formatRow($meta, $row); + } + }))->withCacheLimit($result->getFetchSize()); - return new CypherList($tbr); - } + $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']])); + } + }); - /** - * @psalm-mutation-free - */ - public function decorateRequest(RequestInterface $request, ConnectionInterface $connection): RequestInterface - { - return $this->formatter->decorateRequest($request, $connection); + return $tbr; } /** * @psalm-mutation-free */ - public function statementConfigOverride(ConnectionInterface $connection): array + private function formatRow(array $meta, array $result): CypherMap { - return array_merge($this->formatter->statementConfigOverride($connection), [ - 'includeStats' => true, - ]); + /** @var array $map */ + $map = []; + if (!array_key_exists('fields', $meta)) { + return new CypherMap($map); + } + + foreach ($meta['fields'] as $i => $column) { + $map[$column] = $this->translator->mapValueToType($result[$i]); + } + + return new CypherMap($map); } } diff --git a/src/Http/HttpHelper.php b/src/Http/HttpHelper.php index 822e6880..9f059be7 100644 --- a/src/Http/HttpHelper.php +++ b/src/Http/HttpHelper.php @@ -13,6 +13,7 @@ namespace Laudis\Neo4j\Http; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use function array_key_first; use function array_merge; use function count; @@ -23,7 +24,6 @@ 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; @@ -36,7 +36,7 @@ /** * Helper functions for the http protocol. * - * @psalm-import-type CypherResponseSet from \Laudis\Neo4j\Contracts\FormatterInterface + * @psalm-import-type CypherResponseSet from SummarizedResultFormatter */ final class HttpHelper { @@ -198,7 +198,7 @@ public static function splitJoltSingleton(stdClass $joltSingleton): array * * @throws JsonException */ - public static function statementsToJson(ConnectionInterface $connection, FormatterInterface $formatter, iterable $statements): string + public static function statementsToJson(ConnectionInterface $connection, SummarizedResultFormatter $formatter, iterable $statements): string { $tbr = []; foreach ($statements as $statement) { @@ -207,7 +207,11 @@ public static function statementsToJson(ConnectionInterface $connection, Formatt 'resultDataContents' => [], 'includeStats' => false, ]; - $st = array_merge($st, $formatter->statementConfigOverride($connection)); + /** @noinspection PhpUndefinedMethodInspection */ + $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; diff --git a/src/Http/HttpUnmanagedTransaction.php b/src/Http/HttpUnmanagedTransaction.php index 7de93457..48c0a3a9 100644 --- a/src/Http/HttpUnmanagedTransaction.php +++ b/src/Http/HttpUnmanagedTransaction.php @@ -13,11 +13,12 @@ namespace Laudis\Neo4j\Http; +use Laudis\Neo4j\Databags\SummarizedResult; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; 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; @@ -44,8 +45,6 @@ final class HttpUnmanagedTransaction implements UnmanagedTransactionInterface /** * @psalm-mutation-free - * - * @param FormatterInterface $formatter */ public function __construct( /** @psalm-readonly */ @@ -57,15 +56,15 @@ public function __construct( /** * @psalm-readonly */ - private readonly FormatterInterface $formatter + private readonly SummarizedResultFormatter $formatter ) {} - public function run(string $statement, iterable $parameters = []) + public function run(string $statement, iterable $parameters = []): SummarizedResult { return $this->runStatement(new Statement($statement, $parameters)); } - public function runStatement(Statement $statement) + public function runStatement(Statement $statement): SummarizedResult { return $this->runStatements([$statement])->first(); } diff --git a/src/Neo4j/Neo4jDriver.php b/src/Neo4j/Neo4jDriver.php index 004be4d7..321092fe 100644 --- a/src/Neo4j/Neo4jDriver.php +++ b/src/Neo4j/Neo4jDriver.php @@ -26,11 +26,10 @@ use Laudis\Neo4j\Contracts\AddressResolverInterface; 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 Psr\Log\LogLevel; @@ -41,26 +40,22 @@ * * @implements DriverInterface * - * @psalm-import-type OGMResults from OGMFormatter + * @psalm-import-type OGMResults from SummarizedResultFormatter */ final class Neo4jDriver implements DriverInterface { /** - * @param FormatterInterface $formatter - * * @psalm-mutation-free */ public function __construct( private readonly UriInterface $parsedUrl, private readonly Neo4jConnectionPool $pool, - private readonly FormatterInterface $formatter + private readonly SummarizedResultFormatter $formatter ) {} /** * @template U * - * @param FormatterInterface $formatter - * * @return ( * func_num_args() is 5 * ? self @@ -69,7 +64,7 @@ public function __construct( * * @psalm-suppress MixedReturnTypeCoercion */ - public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null, ?FormatterInterface $formatter = null, ?AddressResolverInterface $resolver = null): self + public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null, ?SummarizedResultFormatter $formatter = null, ?AddressResolverInterface $resolver = null): self { if (is_string($uri)) { $uri = Uri::create($uri); @@ -84,7 +79,7 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co return new self( $uri, Neo4jConnectionPool::create($uri, $authenticate, $configuration, $resolver, $semaphore), - $formatter ?? OGMFormatter::create(), + $formatter ?? SummarizedResultFormatter::create(), ); } diff --git a/src/Types/AbstractPropertyObject.php b/src/Types/AbstractPropertyObject.php index 3b37ee9c..673e6cf9 100644 --- a/src/Types/AbstractPropertyObject.php +++ b/src/Types/AbstractPropertyObject.php @@ -15,11 +15,12 @@ use BadMethodCallException; use Laudis\Neo4j\Contracts\HasPropertiesInterface; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use function sprintf; /** - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter * * @template PropertyTypes * @template ObjectTypes diff --git a/src/Types/CypherMap.php b/src/Types/CypherMap.php index 2a78832d..ebbdc028 100644 --- a/src/Types/CypherMap.php +++ b/src/Types/CypherMap.php @@ -35,7 +35,7 @@ public function getAsCypherMap(string $key, mixed $default = null): CypherMap if (func_num_args() === 1) { $value = $this->get($key); } else { - /** @var mixed */ + /** @var mixed $value */ $value = $this->get($key, $default); } $tbr = TypeCaster::toCypherMap($value); @@ -54,7 +54,7 @@ public function getAsCypherList(string $key, mixed $default = null): CypherList if (func_num_args() === 1) { $value = $this->get($key); } else { - /** @var mixed */ + /** @var mixed $value */ $value = $this->get($key, $default); } $tbr = TypeCaster::toCypherList($value); diff --git a/src/Types/Node.php b/src/Types/Node.php index b747781b..0c660714 100644 --- a/src/Types/Node.php +++ b/src/Types/Node.php @@ -14,13 +14,14 @@ namespace Laudis\Neo4j\Types; use Laudis\Neo4j\Exception\PropertyDoesNotExistException; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use function sprintf; /** * A Node class representing a Node in cypher. * - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter * * @psalm-immutable * @psalm-immutable diff --git a/src/Types/Relationship.php b/src/Types/Relationship.php index 280a6679..0528d58d 100644 --- a/src/Types/Relationship.php +++ b/src/Types/Relationship.php @@ -13,10 +13,12 @@ namespace Laudis\Neo4j\Types; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; + /** * A Relationship class representing a Relationship in cypher. * - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter * * @psalm-immutable */ diff --git a/src/Types/UnboundRelationship.php b/src/Types/UnboundRelationship.php index 6f7a53a4..1148c960 100644 --- a/src/Types/UnboundRelationship.php +++ b/src/Types/UnboundRelationship.php @@ -14,13 +14,14 @@ namespace Laudis\Neo4j\Types; use Laudis\Neo4j\Exception\PropertyDoesNotExistException; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use function sprintf; /** * A relationship without any nodes attached to it. * - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter * * @psalm-immutable * diff --git a/tests/Integration/BoltDriverIntegrationTest.php b/tests/Integration/BoltDriverIntegrationTest.php index 2b449cf5..31987e9a 100644 --- a/tests/Integration/BoltDriverIntegrationTest.php +++ b/tests/Integration/BoltDriverIntegrationTest.php @@ -16,7 +16,6 @@ use Bolt\error\ConnectException; use Exception; use Laudis\Neo4j\Bolt\BoltDriver; -use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Neo4j\Neo4jDriver; use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest; use Throwable; @@ -84,7 +83,6 @@ public function testBookmarkUpdates(): void $this->assertTrue($bookmark->isEmpty()); $previousBookmark = $bookmark; - /** @var SummarizedResult $result */ $result = $session->run('MATCH (x) RETURN x'); $result->preload(); @@ -93,7 +91,6 @@ public function testBookmarkUpdates(): void $this->assertNotEquals($previousBookmark->values(), $bookmark->values()); $previousBookmark = $bookmark; - /** @var SummarizedResult $result */ $result = $session->run('CREATE (x:Node)'); $result->preload(); diff --git a/tests/Integration/ClientIntegrationTest.php b/tests/Integration/ClientIntegrationTest.php index 1e1e072a..e47bd4fa 100644 --- a/tests/Integration/ClientIntegrationTest.php +++ b/tests/Integration/ClientIntegrationTest.php @@ -127,19 +127,19 @@ public function testAvailabilityFullImplementation(): void public function testTransactionFunction(): void { $result = $this->getSession()->transaction( - static fn (TransactionInterface $tsx) => $tsx->run('UNWIND [1] AS x RETURN x')->first()->getAsInt('x') + static fn (TransactionInterface $tsx): int => $tsx->run('UNWIND [1] AS x RETURN x')->first()->getAsInt('x') ); self::assertEquals(1, $result); $result = $this->getSession()->readTransaction( - static fn (TransactionInterface $tsx) => $tsx->run('UNWIND [1] AS x RETURN x')->first()->getAsInt('x') + static fn (TransactionInterface $tsx): int => $tsx->run('UNWIND [1] AS x RETURN x')->first()->getAsInt('x') ); self::assertEquals(1, $result); $result = $this->getSession()->writeTransaction( - static fn (TransactionInterface $tsx) => $tsx->run('UNWIND [1] AS x RETURN x')->first()->getAsInt('x') + static fn (TransactionInterface $tsx): int => $tsx->run('UNWIND [1] AS x RETURN x')->first()->getAsInt('x') ); self::assertEquals(1, $result); @@ -346,7 +346,6 @@ public function testRedundantAcquire(): void $driver = $this->getDriver('bolt'); $reflection = new ReflectionClass($driver); $property = $reflection->getProperty('driver'); - $property->setAccessible(true); /** @var DriverInterface $driver */ $driver = $property->getValue($driver); @@ -356,13 +355,11 @@ public function testRedundantAcquire(): void $reflection = new ReflectionClass($driver); $poolProp = $reflection->getProperty('pool'); - $poolProp->setAccessible(true); /** @var ConnectionPool $pool */ $pool = $poolProp->getValue($driver); $reflection = new ReflectionClass($pool); $connectionProp = $reflection->getProperty('activeConnections'); - $connectionProp->setAccessible(true); /** @var array $activeConnections */ $activeConnections = $connectionProp->getValue($pool); diff --git a/tests/Integration/ComplexQueryTest.php b/tests/Integration/ComplexQueryTest.php index efcb3709..a0e812d5 100644 --- a/tests/Integration/ComplexQueryTest.php +++ b/tests/Integration/ComplexQueryTest.php @@ -48,8 +48,7 @@ public function testValidListParameterHelper(): void public function testMergeTransactionFunction(): void { $this->expectException(Neo4jException::class); - $this->getSession()->writeTransaction(static fn (TSX $tsx) => /** @psalm-suppress ALL */ -$tsx->run('MERGE (x {y: "z"}:X) return x')->first() + $this->getSession()->writeTransaction(static fn (TSX $tsx): string => $tsx->run('MERGE (x {y: "z"}:X) return x')->first() ->getAsMap('x') ->getAsString('y')); } @@ -126,7 +125,7 @@ public function testPath(): void self::assertEquals( [['attribute' => 'xy'], ['attribute' => 'yz']], /** @psalm-suppress MissingClosureReturnType */ - $result->getAsCypherList('y')->map(static fn ($r) => /** + $result->getAsCypherList('y')->map(static fn (mixed $r): array => /** * @psalm-suppress MixedMethodCall * * @var array diff --git a/tests/Integration/OGMFormatterIntegrationTest.php b/tests/Integration/OGMFormatterIntegrationTest.php deleted file mode 100644 index 185809b7..00000000 --- a/tests/Integration/OGMFormatterIntegrationTest.php +++ /dev/null @@ -1,451 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Tests\Integration; - -use function compact; - -use DateInterval; - -use function json_encode; - -use Laudis\Neo4j\Contracts\PointInterface; -use Laudis\Neo4j\Contracts\TransactionInterface; -use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest; -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 function range; -use function sprintf; - -/** - * @psalm-suppress MixedArrayAccess - */ -final class OGMFormatterIntegrationTest extends EnvironmentAwareIntegrationTest -{ - public function testNull(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN null as x')); - - self::assertNull($results->first()->get('x')); - } - - public function testList(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN range(5, 15) as list, range(16, 35) as list2')); - - $list = $results->first()->get('list'); - $list2 = $results->first()->get('list2'); - - self::assertInstanceOf(CypherList::class, $list); - self::assertInstanceOf(CypherList::class, $list2); - self::assertEquals(range(5, 15), $list->toArray()); - self::assertEquals(range(16, 35), $list2->toArray()); - self::assertEquals(json_encode(range(5, 15), JSON_THROW_ON_ERROR), json_encode($list, JSON_THROW_ON_ERROR)); - self::assertEquals(json_encode(range(16, 35), JSON_THROW_ON_ERROR), json_encode($list2, JSON_THROW_ON_ERROR)); - } - - public function testMap(): void - { - $map = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN {a: "b", c: "d"} as map')->first()->get('map')); - self::assertInstanceOf(CypherMap::class, $map); - $array = $map->toArray(); - ksort($array); - self::assertEquals(['a' => 'b', 'c' => 'd'], $array); - } - - public function testBoolean(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN true as bool1, false as bool2')); - - self::assertEquals(1, $results->count()); - self::assertIsBool($results->first()->get('bool1')); - self::assertIsBool($results->first()->get('bool2')); - } - - public function testInteger(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<count()); - self::assertEquals(1, $results[0]['x.num']); - self::assertEquals(2, $results[1]['x.num']); - self::assertEquals(3, $results[2]['x.num']); - } - - public function testFloat(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN 0.1 AS float')); - - self::assertIsFloat($results->first()->get('float')); - } - - public function testString(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN "abc" AS string')); - - self::assertIsString($results->first()->get('string')); - } - - public function testDate(): void - { - $results = $this->getSession()->transaction(function (TransactionInterface $tsx) { - $query = $this->articlesQuery(); - $query .= 'RETURN article.datePublished as published_at'; - - return $tsx->run($query); - }); - - self::assertEquals(3, $results->count()); - - $publishedAt = $results[0]['published_at']; - self::assertInstanceOf(Date::class, $publishedAt); - self::assertEquals(18048, $publishedAt->getDays()); - self::assertEquals( - json_encode(['days' => 18048], JSON_THROW_ON_ERROR), - json_encode($publishedAt, JSON_THROW_ON_ERROR)); - self::assertEquals(18048, $publishedAt->days); - - self::assertInstanceOf(Date::class, $results[1]['published_at']); - self::assertEquals(18049, $results[1]['published_at']->getDays()); - self::assertEquals( - json_encode(['days' => 18049], JSON_THROW_ON_ERROR), - json_encode($results[1]['published_at'], JSON_THROW_ON_ERROR)); - - self::assertInstanceOf(Date::class, $results[2]['published_at']); - self::assertEquals(18742, $results[2]['published_at']->getDays()); - self::assertEquals( - json_encode(['days' => 18742], JSON_THROW_ON_ERROR), - json_encode($results[2]['published_at'], JSON_THROW_ON_ERROR)); - } - - public function testTime(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN time("12:00:00.000000000") AS time')); - - $time = $results->first()->get('time'); - self::assertInstanceOf(Time::class, $time); - self::assertEquals(12.0 * 60 * 60 * 1_000_000_000, $time->getNanoSeconds()); - self::assertEquals(12.0 * 60 * 60 * 1_000_000_000, $time->nanoSeconds); - } - - public function testLocalTime(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN localtime("12") AS time')); - - /** @var LocalTime $time */ - $time = $results->first()->get('time'); - self::assertInstanceOf(LocalTime::class, $time); - self::assertEquals(43_200_000_000_000, $time->getNanoseconds()); - - $results = $this->getSession()->run('RETURN localtime("09:23:42.000") AS time', []); - - /** @var LocalTime $time */ - $time = $results->first()->get('time'); - self::assertInstanceOf(LocalTime::class, $time); - self::assertEquals(33_822_000_000_000, $time->getNanoseconds()); - self::assertEquals(33_822_000_000_000, $time->nanoseconds); - } - - public function testDateTime(): void - { - $results = $this->getSession()->transaction(function (TransactionInterface $tsx) { - $query = $this->articlesQuery(); - $query .= 'RETURN article.created as created_at'; - - return $tsx->run($query); - }); - - self::assertEquals(3, $results->count()); - - $createdAt = $results[0]['created_at']; - self::assertInstanceOf(DateTime::class, $createdAt); - if ($createdAt->isLegacy()) { - self::assertEquals(1_559_414_432, $createdAt->getSeconds()); - } else { - self::assertEquals(1_559_410_832, $createdAt->getSeconds()); - } - - self::assertEquals(142_000_000, $createdAt->getNanoseconds()); - self::assertEquals(3600, $createdAt->getTimeZoneOffsetSeconds()); - self::assertEquals(142_000_000, $createdAt->getNanoseconds()); - self::assertEquals(3600, $createdAt->getTimeZoneOffsetSeconds()); - - if ($createdAt->isLegacy()) { - self::assertEquals('{"seconds":1559414432,"nanoseconds":142000000,"tzOffsetSeconds":3600}', json_encode($createdAt, JSON_THROW_ON_ERROR)); - } else { - self::assertEquals('{"seconds":1559410832,"nanoseconds":142000000,"tzOffsetSeconds":3600}', json_encode($createdAt, JSON_THROW_ON_ERROR)); - } - } - - public function testLocalDateTime(): void - { - $result = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN localdatetime() as local')->first()->get('local')); - - self::assertInstanceOf(LocalDateTime::class, $result); - $date = $result->toDateTime(); - self::assertEquals($result->getSeconds(), $date->getTimestamp()); - } - - public function testDuration(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<count()); - self::assertEquals(new Duration(0, 14, 58320, 0), $results[0]['aDuration']); - $duration = $results[1]['aDuration']; - self::assertInstanceOf(Duration::class, $duration); - self::assertEquals(new Duration(5, 1, 43200, 0), $duration); - self::assertEquals(5, $duration->getMonths()); - self::assertEquals(1, $duration->getDays()); - self::assertEquals(43200, $duration->getSeconds()); - self::assertEquals(0, $duration->getNanoseconds()); - self::assertEquals(new Duration(0, 22, 71509, 500_000_000), $results[2]['aDuration']); - self::assertEquals(new Duration(0, 17, 43200, 0), $results[3]['aDuration']); - self::assertEquals(new Duration(0, 0, 91, 123_456_789), $results[4]['aDuration']); - self::assertEquals(new Duration(0, 0, 91, 123_456_789), $results[5]['aDuration']); - - self::assertEquals(5, $duration->getMonths()); - self::assertEquals(1, $duration->getDays()); - self::assertEquals(43200, $duration->getSeconds()); - self::assertEquals(0, $duration->getNanoseconds()); - $interval = new DateInterval(sprintf('P%dM%dDT%dS', 5, 1, 43200)); - self::assertEquals($interval, $duration->toDateInterval()); - self::assertEquals('{"months":5,"days":1,"seconds":43200,"nanoseconds":0}', json_encode($duration, JSON_THROW_ON_ERROR)); - } - - public function testPoint(): void - { - $result = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN point({x: 3, y: 4}) AS point')); - self::assertInstanceOf(CypherList::class, $result); - $row = $result->first(); - self::assertInstanceOf(CypherMap::class, $row); - $point = $row->get('point'); - - self::assertInstanceOf(CartesianPoint::class, $point); - self::assertEquals(3.0, $point->getX()); - self::assertEquals(4.0, $point->getY()); - self::assertEquals('cartesian', $point->getCrs()); - self::assertGreaterThan(0, $point->getSrid()); - self::assertEquals(3.0, $point->x); - self::assertEquals(4.0, $point->y); - self::assertEquals('cartesian', $point->crs); - self::assertGreaterThan(0, $point->srid); - self::assertEquals( - json_encode([ - 'x' => 3, - 'y' => 4, - 'crs' => 'cartesian', - 'srid' => 7203, - ], JSON_THROW_ON_ERROR), - json_encode($point, JSON_THROW_ON_ERROR) - ); - } - - public function testNode(): void - { - $uuid = 'cc60fd69-a92b-47f3-9674-2f27f3437d66'; - $email = 'a@b.c'; - $type = 'pepperoni'; - - $arguments = compact('email', 'uuid', 'type'); - - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run( - 'MERGE (u:User{email: $email})-[:LIKES]->(p:Food:Pizza {type: $type}) ON CREATE SET u.uuid=$uuid RETURN u, p', - $arguments - )); - - self::assertEquals(1, $results->count()); - - /** @var Node $u */ - $u = $results[0]['u']; - self::assertInstanceOf(Node::class, $u); - self::assertEquals(['User'], $u->getLabels()->toArray()); - self::assertEquals($email, $u->getProperties()['email']); - self::assertEquals($uuid, $u->getProperties()['uuid']); - self::assertEquals($email, $u->email); - self::assertEquals($uuid, $u->uuid); - self::assertEquals( - json_encode([ - 'id' => $u->getId(), - 'labels' => $u->getLabels()->jsonSerialize(), - 'properties' => $u->getProperties()->jsonSerialize(), - ], JSON_THROW_ON_ERROR), - json_encode($u, JSON_THROW_ON_ERROR)); - - /** @var Node $p */ - $p = $results[0]['p']; - self::assertInstanceOf(Node::class, $p); - self::assertEquals(['Food', 'Pizza'], $p->getLabels()->toArray()); - self::assertEquals($type, $p->getProperties()['type']); - self::assertEquals( - json_encode([ - 'id' => $p->getId(), - 'labels' => $p->getLabels()->jsonSerialize(), - 'properties' => $p->getProperties()->jsonSerialize(), - ], JSON_THROW_ON_ERROR), - json_encode($p, JSON_THROW_ON_ERROR) - ); - } - - public function testRelationship(): void - { - $result = $this->getSession()->transaction(static function (TransactionInterface $tsx) { - $tsx->run('MATCH (n) DETACH DELETE n'); - - return $tsx->run('MERGE (x:X {x: 1}) - [xy:XY {x: 1, y: 1}] -> (y:Y {y: 1}) RETURN xy')->first()->get('xy'); - }); - - self::assertInstanceOf(Relationship::class, $result); - self::assertEquals('XY', $result->getType()); - self::assertEquals(['x' => 1, 'y' => 1], $result->getProperties()->toArray()); - self::assertEquals(1, $result->x); - self::assertEquals(1, $result->y); - self::assertEquals( - json_encode([ - 'id' => $result->getId(), - 'type' => $result->getType(), - 'properties' => $result->getProperties(), - 'startNodeId' => $result->getStartNodeId(), - 'endNodeId' => $result->getEndNodeId(), - ], JSON_THROW_ON_ERROR), - json_encode($result, JSON_THROW_ON_ERROR) - ); - } - - public function testPath(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<<'CYPHER' -MERGE (b:Node {x:$x}) - [:HasNode {attribute: $xy}] -> (:Node {y:$y}) - [:HasNode {attribute: $yz}] -> (:Node {z:$z}) -WITH b -MATCH (x:Node) - [y:HasNode*2] -> (z:Node) -RETURN x, y, z -CYPHER, ['x' => 'x', 'xy' => 'xy', 'y' => 'y', 'yz' => 'yz', 'z' => 'z'])); - - self::assertEquals(1, $results->count()); - } - - 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}) -RETURN path -CYPHER, ['x' => 'x', 'xy' => 'xy', 'y' => 'y', 'yz' => 'yz', 'z' => 'z'])); - - self::assertEquals(1, $results->count()); - $path = $results->first()->get('path'); - - self::assertInstanceOf(Path::class, $path); - self::assertCount(2, $path->getRelationships()); - self::assertCount(3, $path->getNodes()); - - self::assertEquals(['x' => 'x'], $path->getNodes()->get(0)->getProperties()->toArray()); - self::assertEquals(['y' => 'y'], $path->getNodes()->get(1)->getProperties()->toArray()); - self::assertEquals(['z' => 'z'], $path->getNodes()->get(2)->getProperties()->toArray()); - self::assertEquals(['attribute' => 'xy'], $path->getRelationships()->get(0)->getProperties()->toArray()); - self::assertEquals(['attribute' => 'yz'], $path->getRelationships()->get(1)->getProperties()->toArray()); - } - - public function testPropertyTypes(): void - { - $result = $this->getSession(['neo4j', 'bolt'])->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<first()->get('a'); - - self::assertInstanceOf(Node::class, $node); - self::assertInstanceOf(PointInterface::class, $node->thePoint); - self::assertInstanceOf(CypherList::class, $node->theList); - self::assertInstanceOf(Date::class, $node->theDate); - self::assertInstanceOf(DateTime::class, $node->theDateTime); - self::assertInstanceOf(Duration::class, $node->theDuration); - self::assertInstanceOf(LocalDateTime::class, $node->theLocalDateTime); - self::assertInstanceOf(LocalTime::class, $node->theLocalTime); - self::assertInstanceOf(Time::class, $node->theTime); - } - - private function articlesQuery(): string - { - return << Date: Fri, 24 Jan 2025 16:07:02 +0530 Subject: [PATCH 03/10] feat: Remove OGMFormatter and FormatterInterface --- src/Basic/Client.php | 7 - src/Basic/Driver.php | 6 - src/Basic/Session.php | 13 +- src/Basic/UnmanagedTransaction.php | 7 - src/Bolt/BoltDriver.php | 27 +- src/Bolt/BoltResult.php | 4 +- src/Bolt/BoltUnmanagedTransaction.php | 20 +- src/Bolt/Session.php | 3 + src/Client.php | 21 +- src/ClientBuilder.php | 32 +-- src/Common/DriverSetupManager.php | 8 +- src/Common/TransactionHelper.php | 5 +- src/Contracts/ClientInterface.php | 2 +- src/Contracts/DriverInterface.php | 8 +- src/Contracts/SessionInterface.php | 2 +- src/Contracts/TransactionInterface.php | 2 +- .../UnmanagedTransactionInterface.php | 3 +- src/DriverFactory.php | 41 +-- src/Formatter/BasicFormatter.php | 235 ------------------ src/Formatter/OGMFormatter.php | 0 src/Formatter/SummarizedResultFormatter.php | 44 ++-- src/Neo4j/Neo4jDriver.php | 12 - src/ParameterHelper.php | 2 + src/Types/AbstractCypherSequence.php | 2 + src/Types/CypherMap.php | 2 + 25 files changed, 74 insertions(+), 434 deletions(-) delete mode 100644 src/Formatter/BasicFormatter.php delete mode 100644 src/Formatter/OGMFormatter.php diff --git a/src/Basic/Client.php b/src/Basic/Client.php index afb6db66..d6df61b4 100644 --- a/src/Basic/Client.php +++ b/src/Basic/Client.php @@ -18,16 +18,9 @@ use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; -/** - * @implements ClientInterface> - */ final class Client implements ClientInterface { - /** - * @param ClientInterface> $client - */ public function __construct( private readonly ClientInterface $client ) {} diff --git a/src/Basic/Driver.php b/src/Basic/Driver.php index 7b73a876..f84f8eee 100644 --- a/src/Basic/Driver.php +++ b/src/Basic/Driver.php @@ -23,14 +23,9 @@ 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( @@ -52,7 +47,6 @@ public function verifyConnectivity(?SessionConfiguration $config = null): bool 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 index cb31416f..8052fc4a 100644 --- a/src/Basic/Session.php +++ b/src/Basic/Session.php @@ -21,14 +21,8 @@ use Laudis\Neo4j\Types\CypherList; use Laudis\Neo4j\Types\CypherMap; -/** - * @implements SessionInterface> - */ final class Session implements SessionInterface { - /** - * @param SessionInterface> $session - */ public function __construct( private readonly SessionInterface $session ) {} @@ -36,16 +30,13 @@ public function __construct( /** * @param iterable $statements * - * @return CypherList> + * @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); @@ -53,8 +44,6 @@ public function runStatement(Statement $statement, ?TransactionConfiguration $co /** * @param iterable $parameters - * - * @return SummarizedResult */ public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null): SummarizedResult { diff --git a/src/Basic/UnmanagedTransaction.php b/src/Basic/UnmanagedTransaction.php index beef7e1e..03a4631d 100644 --- a/src/Basic/UnmanagedTransaction.php +++ b/src/Basic/UnmanagedTransaction.php @@ -17,16 +17,9 @@ use Laudis\Neo4j\Databags\Statement; use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; -/** - * @implements UnmanagedTransactionInterface> - */ final class UnmanagedTransaction implements UnmanagedTransactionInterface { - /** - * @param UnmanagedTransactionInterface> $tsx - */ public function __construct( private readonly UnmanagedTransactionInterface $tsx ) {} diff --git a/src/Bolt/BoltDriver.php b/src/Bolt/BoltDriver.php index 7c21c484..a3625a2f 100644 --- a/src/Bolt/BoltDriver.php +++ b/src/Bolt/BoltDriver.php @@ -23,50 +23,33 @@ 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 Psr\Log\LogLevel; /** * Drives a singular bolt connections. * - * @template T - * - * @implements DriverInterface - * - * @psalm-import-type OGMResults from OGMFormatter + * @psalm-import-type OGMResults from SummarizedResultFormatter */ final class BoltDriver implements DriverInterface { /** - * @param FormatterInterface $formatter - * * @psalm-mutation-free */ public function __construct( private readonly UriInterface $parsedUrl, private readonly ConnectionPool $pool, - private readonly FormatterInterface $formatter + private readonly 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, ?SummarizedResultFormatter $formatter = null): self { if (is_string($uri)) { $uri = Uri::create($uri); @@ -80,7 +63,7 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co return new self( $uri, ConnectionPool::create($uri, $authenticate, $configuration, $semaphore), - $formatter ?? OGMFormatter::create(), + $formatter ?? SummarizedResultFormatter::create(), ); } diff --git a/src/Bolt/BoltResult.php b/src/Bolt/BoltResult.php index dd243b28..81c491ab 100644 --- a/src/Bolt/BoltResult.php +++ b/src/Bolt/BoltResult.php @@ -13,6 +13,7 @@ namespace Laudis\Neo4j\Bolt; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use function array_splice; use function count; @@ -21,10 +22,9 @@ use function in_array; use Iterator; -use Laudis\Neo4j\Contracts\FormatterInterface; /** - * @psalm-import-type BoltCypherStats from FormatterInterface + * @psalm-import-type BoltCypherStats from SummarizedResultFormatter * * @implements Iterator */ diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index d8254348..80692275 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -18,6 +18,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\Enum\TransactionState; use Laudis\Neo4j\Exception\ClientException; @@ -33,10 +34,6 @@ /** * Manages a transaction over the bolt protocol. * - * @template T - * - * @implements UnmanagedTransactionInterface - * * @psalm-import-type BoltMeta from SummarizedResultFormatter */ final class BoltUnmanagedTransaction implements UnmanagedTransactionInterface @@ -59,6 +56,8 @@ public function __construct( /** * @throws ClientException|Throwable + * + * @return CypherList */ public function commit(iterable $statements = []): CypherList { @@ -78,10 +77,8 @@ 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 function (CypherList $list) { + $list->preload(); }); $this->connection->commit(); @@ -113,7 +110,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)); } @@ -121,7 +118,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); @@ -159,10 +156,11 @@ public function runStatement(Statement $statement) /** * @throws Throwable + * + * @return CypherList */ public function runStatements(iterable $statements): CypherList { - /** @var list $tbr */ $tbr = []; foreach ($statements as $statement) { $tbr[] = $this->runStatement($statement); diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php index f8042597..137a44c3 100644 --- a/src/Bolt/Session.php +++ b/src/Bolt/Session.php @@ -59,6 +59,9 @@ public function __construct( $this->bookmarkHolder = new BookmarkHolder(Bookmark::from($config->getBookmarks())); } + /** + * @return CypherList + */ public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList { $tbr = []; diff --git a/src/Client.php b/src/Client.php index 9d862061..7e5aaa54 100644 --- a/src/Client.php +++ b/src/Client.php @@ -21,33 +21,30 @@ use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; 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\Types\CypherList; /** * A collection of drivers with methods to run queries though them. - * - * @template ResultFormat - * - * @implements ClientInterface */ final class Client implements ClientInterface { /** - * @var array>> + * @var array> */ private array $boundTransactions = []; /** - * @var array> + * @var array */ private array $boundSessions = []; /** * @psalm-mutation-free * - * @param DriverSetupManager $driverSetups + * @param DriverSetupManager $driverSetups */ public function __construct( private readonly DriverSetupManager $driverSetups, @@ -70,12 +67,12 @@ public function getDefaultTransactionConfiguration(): TransactionConfiguration return $this->defaultTransactionConfiguration; } - public function run(string $statement, iterable $parameters = [], ?string $alias = null) + public function run(string $statement, iterable $parameters = [], ?string $alias = null): SummarizedResult { return $this->runStatement(Statement::create($statement, $parameters), $alias); } - public function runStatement(Statement $statement, ?string $alias = null) + public function runStatement(Statement $statement, ?string $alias = null): SummarizedResult { return $this->runStatements([$statement], $alias)->first(); } @@ -86,6 +83,7 @@ private function getRunner(?string $alias = null): TransactionInterface|SessionI if (array_key_exists($alias, $this->boundTransactions) && count($this->boundTransactions[$alias]) > 0) { + /** @psalm-suppress PossiblyNullArrayOffset */ return $this->boundTransactions[$alias][array_key_last($this->boundTransactions[$alias])]; } @@ -126,9 +124,6 @@ public function getDriver(?string $alias): DriverInterface return $this->driverSetups->getDriver($this->defaultSessionConfiguration, $alias); } - /** - * @return SessionInterface - */ private function startSession(?string $alias, SessionConfiguration $configuration): SessionInterface { return $this->getDriver($alias)->createSession($configuration); @@ -187,7 +182,7 @@ public function rollbackBoundTransaction(?string $alias = null, int $depth = 1): } /** - * @param callable(UnmanagedTransactionInterface): void $handler + * @param callable(UnmanagedTransactionInterface): void $handler */ private function popTransactions(callable $handler, ?string $alias = null, int $depth = 1): void { diff --git a/src/ClientBuilder.php b/src/ClientBuilder.php index 0ec4b497..791c8d5f 100644 --- a/src/ClientBuilder.php +++ b/src/ClientBuilder.php @@ -20,14 +20,12 @@ use Laudis\Neo4j\Common\Uri; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Contracts\ClientInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\DriverConfiguration; use Laudis\Neo4j\Databags\DriverSetup; use Laudis\Neo4j\Databags\SessionConfiguration; use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Exception\UnsupportedScheme; -use Laudis\Neo4j\Formatter\OGMFormatter; use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Types\CypherMap; use Psr\Log\LoggerInterface; @@ -35,9 +33,7 @@ /** * Immutable factory for creating a client. * - * @template T - * - * @psalm-import-type OGMTypes from OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter */ final class ClientBuilder { @@ -46,7 +42,7 @@ final class ClientBuilder /** * @psalm-mutation-free * - * @param DriverSetupManager $driverSetups + * @param DriverSetupManager $driverSetups */ public function __construct( /** @psalm-readonly */ @@ -58,8 +54,6 @@ public function __construct( /** * Creates a client builder with default configurations and an OGMFormatter. - * - * @return ClientBuilder>> */ public static function create(?string $logLevel = null, ?LoggerInterface $logger = null): ClientBuilder { @@ -77,8 +71,6 @@ public static function create(?string $logLevel = null, ?LoggerInterface $logger /** * @psalm-mutation-free - * - * @return self */ public function withDriver(string $alias, string $url, ?AuthenticateInterface $authentication = null, ?int $priority = 0): self { @@ -91,8 +83,6 @@ public function withDriver(string $alias, string $url, ?AuthenticateInterface $a /** * @psalm-external-mutation-free - * - * @return self */ private function withParsedUrl(string $alias, Uri $uri, AuthenticateInterface $authentication, int $priority): self { @@ -111,8 +101,6 @@ private function withParsedUrl(string $alias, Uri $uri, AuthenticateInterface $a /** * Sets the default connection to the given alias. * - * @return self - * * @psalm-mutation-free */ public function withDefaultDriver(string $alias): self @@ -124,15 +112,9 @@ public function withDefaultDriver(string $alias): self } /** - * @template U - * - * @param FormatterInterface $formatter - * - * @return self - * * @psalm-mutation-free */ - public function withFormatter(FormatterInterface $formatter): self + public function withFormatter(SummarizedResultFormatter $formatter): self { return new self( $this->defaultSessionConfig, @@ -142,8 +124,6 @@ public function withFormatter(FormatterInterface $formatter): self } /** - * @return ClientInterface - * * @psalm-mutation-free */ public function build(): ClientInterface @@ -156,8 +136,6 @@ public function build(): ClientInterface } /** - * @return self - * * @psalm-mutation-free */ public function withDefaultDriverConfiguration(DriverConfiguration $config): self @@ -170,8 +148,6 @@ public function withDefaultDriverConfiguration(DriverConfiguration $config): sel } /** - * @return self - * * @psalm-mutation-free */ public function withDefaultSessionConfiguration(SessionConfiguration $config): self @@ -183,8 +159,6 @@ public function withDefaultSessionConfiguration(SessionConfiguration $config): s } /** - * @return self - * * @psalm-mutation-free */ public function withDefaultTransactionConfiguration(TransactionConfiguration $config): self diff --git a/src/Common/DriverSetupManager.php b/src/Common/DriverSetupManager.php index 78274b17..28b5646b 100644 --- a/src/Common/DriverSetupManager.php +++ b/src/Common/DriverSetupManager.php @@ -36,16 +36,13 @@ use function sprintf; -/** - * @template ResultFormat - */ class DriverSetupManager implements Countable { private const DEFAULT_DRIVER_CONFIG = 'bolt://localhost:7687'; /** @var array> */ private array $driverSetups = []; - /** @var array> */ + /** @var array */ private array $drivers = []; private ?string $default = null; @@ -99,9 +96,6 @@ public function hasDriver(string $alias): bool return array_key_exists($alias, $this->driverSetups); } - /** - * @return DriverInterface - */ public function getDriver(SessionConfiguration $config, ?string $alias = null): DriverInterface { $alias ??= $this->decideAlias($alias); diff --git a/src/Common/TransactionHelper.php b/src/Common/TransactionHelper.php index a9be078e..f756c5fb 100644 --- a/src/Common/TransactionHelper.php +++ b/src/Common/TransactionHelper.php @@ -24,10 +24,9 @@ final class TransactionHelper /** * @template U - * @template T * - * @param callable():UnmanagedTransactionInterface $tsxFactory - * @param callable(TransactionInterface):U $tsxHandler + * @param callable():UnmanagedTransactionInterface $tsxFactory + * @param callable(TransactionInterface):U $tsxHandler * * @return U */ diff --git a/src/Contracts/ClientInterface.php b/src/Contracts/ClientInterface.php index e07a7ec4..389c35b7 100644 --- a/src/Contracts/ClientInterface.php +++ b/src/Contracts/ClientInterface.php @@ -44,7 +44,7 @@ public function runStatement(Statement $statement, ?string $alias = null): Summa * * @throws Neo4jException * - * @return CypherList + * @return CypherList */ public function runStatements(iterable $statements, ?string $alias = null): CypherList; diff --git a/src/Contracts/DriverInterface.php b/src/Contracts/DriverInterface.php index 9d9b0018..a3902963 100644 --- a/src/Contracts/DriverInterface.php +++ b/src/Contracts/DriverInterface.php @@ -14,22 +14,18 @@ namespace Laudis\Neo4j\Contracts; use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Formatter\CypherList; -use Laudis\Neo4j\Formatter\CypherMap; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; /** * The driver creates sessions for carrying out work. * - * @template ResultFormat - * * @psalm-type ParsedUrl = array{host: string, pass: string|null, path: string, port: int, query: array, scheme: string, user: string|null} * @psalm-type BasicDriver = DriverInterface>> */ interface DriverInterface { /** - * @return SessionInterface - * * @psalm-mutation-free */ public function createSession(?SessionConfiguration $config = null): SessionInterface; diff --git a/src/Contracts/SessionInterface.php b/src/Contracts/SessionInterface.php index b7b39e61..1694c9a5 100644 --- a/src/Contracts/SessionInterface.php +++ b/src/Contracts/SessionInterface.php @@ -30,7 +30,7 @@ interface SessionInterface extends TransactionInterface * * @throws Neo4jException * - * @return CypherList + * @return CypherList */ public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList; diff --git a/src/Contracts/TransactionInterface.php b/src/Contracts/TransactionInterface.php index 329af20d..ff615d8f 100644 --- a/src/Contracts/TransactionInterface.php +++ b/src/Contracts/TransactionInterface.php @@ -37,7 +37,7 @@ public function runStatement(Statement $statement): SummarizedResult; * * @throws Neo4jException * - * @return CypherList + * @return CypherList */ public function runStatements(iterable $statements): CypherList; } diff --git a/src/Contracts/UnmanagedTransactionInterface.php b/src/Contracts/UnmanagedTransactionInterface.php index 09dabaa7..f04697a0 100644 --- a/src/Contracts/UnmanagedTransactionInterface.php +++ b/src/Contracts/UnmanagedTransactionInterface.php @@ -14,6 +14,7 @@ namespace Laudis\Neo4j\Contracts; use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Types\CypherList; /** @@ -28,7 +29,7 @@ interface UnmanagedTransactionInterface extends TransactionInterface * * @param iterable $statements * - * @return CypherList + * @return CypherList */ public function commit(iterable $statements = []): CypherList; diff --git a/src/DriverFactory.php b/src/DriverFactory.php index b0627101..32a16cdd 100644 --- a/src/DriverFactory.php +++ b/src/DriverFactory.php @@ -20,10 +20,9 @@ use Laudis\Neo4j\Common\Uri; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Contracts\DriverInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\DriverConfiguration; +use Laudis\Neo4j\Exception\UnsupportedScheme; use Laudis\Neo4j\Formatter\SummarizedResultFormatter; -use Laudis\Neo4j\Http\HttpDriver; use Laudis\Neo4j\Neo4j\Neo4jDriver; use Psr\Http\Message\UriInterface; @@ -35,19 +34,9 @@ final class DriverFactory { /** - * @template U - * - * @param FormatterInterface $formatter - * * @throws UnsupportedScheme - * - * @return ( - * func_num_args() is 4 - * ? DriverInterface - * : DriverInterface - * ) */ - public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null, ?FormatterInterface $formatter = null): DriverInterface + public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null, ?SummarizedResultFormatter $formatter = null): DriverInterface { if (is_string($uri)) { $uri = Uri::create($uri); @@ -67,18 +56,7 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co throw UnsupportedScheme::make($scheme, ['bolt', 'bolt+s', 'bolt+ssc', 'neo4j', 'neo4j+s', 'neo4j+ssc']); } - /** - * @template U - * - * @param FormatterInterface $formatter - * - * @return ( - * func_num_args() is 4 - * ? DriverInterface - * : DriverInterface - * ) - */ - private static function createBoltDriver(string|UriInterface $uri, ?DriverConfiguration $configuration, ?AuthenticateInterface $authenticate, ?FormatterInterface $formatter = null): DriverInterface + private static function createBoltDriver(string|UriInterface $uri, ?DriverConfiguration $configuration, ?AuthenticateInterface $authenticate, ?SummarizedResultFormatter $formatter = null): DriverInterface { if ($formatter !== null) { return BoltDriver::create($uri, $configuration, $authenticate, $formatter); @@ -87,18 +65,7 @@ private static function createBoltDriver(string|UriInterface $uri, ?DriverConfig return BoltDriver::create($uri, $configuration, $authenticate); } - /** - * @template U - * - * @param FormatterInterface $formatter - * - * @return ( - * func_num_args() is 4 - * ? DriverInterface - * : DriverInterface - * ) - */ - private static function createNeo4jDriver(string|UriInterface $uri, ?DriverConfiguration $configuration, ?AuthenticateInterface $authenticate, ?FormatterInterface $formatter = null): DriverInterface + private static function createNeo4jDriver(string|UriInterface $uri, ?DriverConfiguration $configuration, ?AuthenticateInterface $authenticate, ?SummarizedResultFormatter $formatter = null): DriverInterface { if ($formatter !== null) { return Neo4jDriver::create($uri, $configuration, $authenticate, $formatter); diff --git a/src/Formatter/BasicFormatter.php b/src/Formatter/BasicFormatter.php deleted file mode 100644 index 766a0d13..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, qid?: int, t_first: int} $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, qid?: int, t_first: int} $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 e69de29b..00000000 diff --git a/src/Formatter/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php index 42294913..215e2d7f 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -149,27 +149,29 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection { /** @var ResultSummary|null $summary */ $summary = null; - $result->addFinishedCallback(function (array $response) use ($connection, $statement, $runStart, $resultAvailableAfter, &$summary) { - $stats = $this->formatBoltStats($response); - $resultConsumedAfter = microtime(true) - $runStart; - $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() - ) - ); - }); + $result->addFinishedCallback( + /** @param {array{stats?: BoltCypherStats}&array} $response */ + function (mixed $response) use ($connection, $statement, $runStart, $resultAvailableAfter, &$summary) { + $stats = $this->formatBoltStats($response); + $resultConsumedAfter = microtime(true) - $runStart; + $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->processBoltResult($meta, $result, $connection, $runStart, $resultAvailableAfter, $statement, $holder); diff --git a/src/Neo4j/Neo4jDriver.php b/src/Neo4j/Neo4jDriver.php index 321092fe..768d8bf6 100644 --- a/src/Neo4j/Neo4jDriver.php +++ b/src/Neo4j/Neo4jDriver.php @@ -36,10 +36,6 @@ /** * Driver for auto client-side routing. * - * @template T - * - * @implements DriverInterface - * * @psalm-import-type OGMResults from SummarizedResultFormatter */ final class Neo4jDriver implements DriverInterface @@ -54,14 +50,6 @@ public function __construct( ) {} /** - * @template U - * - * @return ( - * func_num_args() is 5 - * ? self - * : self - * ) - * * @psalm-suppress MixedReturnTypeCoercion */ public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null, ?SummarizedResultFormatter $formatter = null, ?AddressResolverInterface $resolver = null): self diff --git a/src/ParameterHelper.php b/src/ParameterHelper.php index 3d7c20fa..0cf857ea 100644 --- a/src/ParameterHelper.php +++ b/src/ParameterHelper.php @@ -78,6 +78,8 @@ public static function asMap(iterable $iterable): CypherMap /** * @return iterable|scalar|stdClass|IStructure|null + * + * @param \DateTime|array|object|stdClass $value */ public static function asParameter( mixed $value, diff --git a/src/Types/AbstractCypherSequence.php b/src/Types/AbstractCypherSequence.php index 1225f45c..8b82e344 100644 --- a/src/Types/AbstractCypherSequence.php +++ b/src/Types/AbstractCypherSequence.php @@ -553,6 +553,8 @@ public function preload(): void /** * @psalm-mutation-free + * + * @param {int|string|object} $key */ protected function isStringable(mixed $key): bool { diff --git a/src/Types/CypherMap.php b/src/Types/CypherMap.php index ebbdc028..5037299e 100644 --- a/src/Types/CypherMap.php +++ b/src/Types/CypherMap.php @@ -33,6 +33,7 @@ final class CypherMap extends Map public function getAsCypherMap(string $key, mixed $default = null): CypherMap { if (func_num_args() === 1) { + /** @var mixed $value */ $value = $this->get($key); } else { /** @var mixed $value */ @@ -52,6 +53,7 @@ public function getAsCypherMap(string $key, mixed $default = null): CypherMap public function getAsCypherList(string $key, mixed $default = null): CypherList { if (func_num_args() === 1) { + /** @var mixed $value */ $value = $this->get($key); } else { /** @var mixed $value */ From 28168684560f8c20e9b62437a7b84356e35a7ce2 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Wed, 29 Jan 2025 12:47:48 +0530 Subject: [PATCH 04/10] doc: Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 48f55aed..4c9040f6 100644 --- a/README.md +++ b/README.md @@ -308,7 +308,7 @@ composer require nyholm/psr7 nyholm/psr7-server kriswallsmith/buzz ## Result formats/hydration -In order to make the results of the bolt protocol and the http uniform, the driver provides a summarizes the results. +In order to make the results of the bolt protocol and the http uniform, the driver provides and summarizes the results. The default formatter is the `\Laudis\Neo4j\Formatters\SummarizedResultFormatter`, which is explained extensively in [the result format section](#accessing-the-results). From 16f970c1d6cb919fb019ed8fbf8f9f7f9615b6f9 Mon Sep 17 00:00:00 2001 From: exaby73 Date: Wed, 29 Jan 2025 12:54:09 +0530 Subject: [PATCH 05/10] fix: Remove create http driver --- src/DriverFactory.php | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/DriverFactory.php b/src/DriverFactory.php index 697d0bdb..32a16cdd 100644 --- a/src/DriverFactory.php +++ b/src/DriverFactory.php @@ -73,26 +73,4 @@ private static function createNeo4jDriver(string|UriInterface $uri, ?DriverConfi return Neo4jDriver::create($uri, $configuration, $authenticate); } - - /** - * @template U - * - * @param FormatterInterface $formatter - * - * @return ( - * func_num_args() is 4 - * ? DriverInterface - * : DriverInterface - * ) - * - * @pure - */ - private static function createHttpDriver(string|UriInterface $uri, ?DriverConfiguration $configuration, ?AuthenticateInterface $authenticate, ?FormatterInterface $formatter = null): DriverInterface - { - if ($formatter !== null) { - return HttpDriver::create($uri, $configuration, $authenticate, $formatter); - } - - return HttpDriver::create($uri, $configuration, $authenticate); - } } From 7f2b456296d480906ef1903d1195a2490c1ad74c Mon Sep 17 00:00:00 2001 From: exaby73 Date: Wed, 29 Jan 2025 12:54:49 +0530 Subject: [PATCH 06/10] chore: Run PHP CS fixer --- src/Basic/Driver.php | 2 -- src/Basic/Session.php | 1 - src/Bolt/BoltResult.php | 2 +- src/Bolt/BoltUnmanagedTransaction.php | 1 - src/Client.php | 2 -- src/ClientBuilder.php | 4 ---- src/Formatter/SummarizedResultFormatter.php | 5 ----- src/ParameterHelper.php | 5 +++-- 8 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/Basic/Driver.php b/src/Basic/Driver.php index f84f8eee..11baad9c 100644 --- a/src/Basic/Driver.php +++ b/src/Basic/Driver.php @@ -17,10 +17,8 @@ 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; final class Driver implements DriverInterface diff --git a/src/Basic/Session.php b/src/Basic/Session.php index 8052fc4a..bdb9015f 100644 --- a/src/Basic/Session.php +++ b/src/Basic/Session.php @@ -19,7 +19,6 @@ use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; final class Session implements SessionInterface { diff --git a/src/Bolt/BoltResult.php b/src/Bolt/BoltResult.php index 81c491ab..e26f4126 100644 --- a/src/Bolt/BoltResult.php +++ b/src/Bolt/BoltResult.php @@ -13,7 +13,6 @@ namespace Laudis\Neo4j\Bolt; -use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use function array_splice; use function count; @@ -22,6 +21,7 @@ use function in_array; use Iterator; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; /** * @psalm-import-type BoltCypherStats from SummarizedResultFormatter diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index 80692275..74797dcf 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -24,7 +24,6 @@ use Laudis\Neo4j\Exception\ClientException; use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\ParameterHelper; -use Laudis\Neo4j\Types\AbstractCypherSequence; use Laudis\Neo4j\Types\CypherList; use function microtime; diff --git a/src/Client.php b/src/Client.php index 7e5aaa54..d055617c 100644 --- a/src/Client.php +++ b/src/Client.php @@ -43,8 +43,6 @@ final class Client implements ClientInterface /** * @psalm-mutation-free - * - * @param DriverSetupManager $driverSetups */ public function __construct( private readonly DriverSetupManager $driverSetups, diff --git a/src/ClientBuilder.php b/src/ClientBuilder.php index 791c8d5f..a562480a 100644 --- a/src/ClientBuilder.php +++ b/src/ClientBuilder.php @@ -23,11 +23,9 @@ use Laudis\Neo4j\Databags\DriverConfiguration; use Laudis\Neo4j\Databags\DriverSetup; use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Exception\UnsupportedScheme; use Laudis\Neo4j\Formatter\SummarizedResultFormatter; -use Laudis\Neo4j\Types\CypherMap; use Psr\Log\LoggerInterface; /** @@ -41,8 +39,6 @@ final class ClientBuilder /** * @psalm-mutation-free - * - * @param DriverSetupManager $driverSetups */ public function __construct( /** @psalm-readonly */ diff --git a/src/Formatter/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php index b97009f1..215e2d7f 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -47,11 +47,6 @@ 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. * diff --git a/src/ParameterHelper.php b/src/ParameterHelper.php index 0cf857ea..0a064f27 100644 --- a/src/ParameterHelper.php +++ b/src/ParameterHelper.php @@ -20,6 +20,7 @@ use function count; use DateInterval; +use DateTime; use DateTimeInterface; use function get_debug_type; @@ -77,9 +78,9 @@ public static function asMap(iterable $iterable): CypherMap } /** - * @return iterable|scalar|stdClass|IStructure|null + * @param DateTime|array|object|stdClass $value * - * @param \DateTime|array|object|stdClass $value + * @return iterable|scalar|stdClass|IStructure|null */ public static function asParameter( mixed $value, From 427efd548d7b360c46f4c9b1f3164458bc01e70f Mon Sep 17 00:00:00 2001 From: exaby73 Date: Wed, 29 Jan 2025 13:30:00 +0530 Subject: [PATCH 07/10] test: Delete all nodes in ComplexQueryTest for a workaround --- tests/Integration/ComplexQueryTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/Integration/ComplexQueryTest.php b/tests/Integration/ComplexQueryTest.php index a0e812d5..b20f4265 100644 --- a/tests/Integration/ComplexQueryTest.php +++ b/tests/Integration/ComplexQueryTest.php @@ -28,6 +28,15 @@ final class ComplexQueryTest extends EnvironmentAwareIntegrationTest { + public function setUp(): void + { + parent::setUp(); + // testPathReturnType will sometimes run after TransactionIntegrationTest::testTransactionRunNoConsumeButSaveResult + // in CI, which will leave a node in the database. This will cause the test to fail. + // This is a workaround to make sure the database is empty before running the test. + $this->getSession()->run('MATCH (n) DETACH DELETE n'); + } + public function testListParameterHelper(): void { $result = $this->getSession()->transaction(static fn (TSX $tsx) => $tsx->run('MATCH (x) WHERE x.slug IN $listOrMap RETURN x', [ From 51a2a783dde143244beeb4fed19c0eaa54868801 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 13 Feb 2025 12:11:58 +0530 Subject: [PATCH 08/10] pulled old tests back in --- .../SummarizedResultFormatterTest.php | 416 ++++++++++++++++++ 1 file changed, 416 insertions(+) diff --git a/tests/Integration/SummarizedResultFormatterTest.php b/tests/Integration/SummarizedResultFormatterTest.php index b42d291b..52b3468e 100644 --- a/tests/Integration/SummarizedResultFormatterTest.php +++ b/tests/Integration/SummarizedResultFormatterTest.php @@ -13,6 +13,18 @@ namespace Laudis\Neo4j\Tests\Integration; +use DateInterval; +use Laudis\Neo4j\Contracts\PointInterface; +use Laudis\Neo4j\Types\CartesianPoint; +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 function bin2hex; use DateTimeImmutable; @@ -114,4 +126,408 @@ public function testDateTime(): void $this->assertInstanceOf(DateTimeZoneId::class, $ls); $this->assertEquals($dt, $ls->toDateTime()); } + + public function testNull(): void + { + $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN null as x')); + + self::assertNull($results->first()->get('x')); + } + + public function testList(): void + { + $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN range(5, 15) as list, range(16, 35) as list2')); + + $list = $results->first()->get('list'); + $list2 = $results->first()->get('list2'); + + self::assertInstanceOf(CypherList::class, $list); + self::assertInstanceOf(CypherList::class, $list2); + self::assertEquals(range(5, 15), $list->toArray()); + self::assertEquals(range(16, 35), $list2->toArray()); + self::assertEquals(json_encode(range(5, 15), JSON_THROW_ON_ERROR), json_encode($list, JSON_THROW_ON_ERROR)); + self::assertEquals(json_encode(range(16, 35), JSON_THROW_ON_ERROR), json_encode($list2, JSON_THROW_ON_ERROR)); + } + + public function testMap(): void + { + $map = $this->getSession()->transaction(static fn (TransactionInterface $tsx): mixed => $tsx->run('RETURN {a: "b", c: "d"} as map')->first()->get('map')); + self::assertInstanceOf(CypherMap::class, $map); + $array = $map->toArray(); + ksort($array); + self::assertEquals(['a' => 'b', 'c' => 'd'], $array); + } + + public function testBoolean(): void + { + $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN true as bool1, false as bool2')); + + self::assertEquals(1, $results->count()); + self::assertIsBool($results->first()->get('bool1')); + self::assertIsBool($results->first()->get('bool2')); + } + + public function testInteger(): void + { + $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<count()); + self::assertEquals(1, $results[0]['x.num']); + self::assertEquals(2, $results[1]['x.num']); + self::assertEquals(3, $results[2]['x.num']); + } + + public function testFloat(): void + { + $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN 0.1 AS float')); + + self::assertIsFloat($results->first()->get('float')); + } + + public function testString(): void + { + $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN "abc" AS string')); + + self::assertIsString($results->first()->get('string')); + } + + public function testDate(): void + { + $results = $this->getSession()->transaction(function (TransactionInterface $tsx) { + $query = $this->articlesQuery(); + $query .= 'RETURN article.datePublished as published_at'; + + return $tsx->run($query); + }); + + self::assertEquals(3, $results->count()); + + $publishedAt = $results[0]['published_at']; + self::assertInstanceOf(Date::class, $publishedAt); + self::assertEquals(18048, $publishedAt->getDays()); + self::assertEquals( + json_encode(['days' => 18048], JSON_THROW_ON_ERROR), + json_encode($publishedAt, JSON_THROW_ON_ERROR)); + self::assertEquals(18048, $publishedAt->days); + + self::assertInstanceOf(Date::class, $results[1]['published_at']); + self::assertEquals(18049, $results[1]['published_at']->getDays()); + self::assertEquals( + json_encode(['days' => 18049], JSON_THROW_ON_ERROR), + json_encode($results[1]['published_at'], JSON_THROW_ON_ERROR)); + + self::assertInstanceOf(Date::class, $results[2]['published_at']); + self::assertEquals(18742, $results[2]['published_at']->getDays()); + self::assertEquals( + json_encode(['days' => 18742], JSON_THROW_ON_ERROR), + json_encode($results[2]['published_at'], JSON_THROW_ON_ERROR)); + } + + public function testTime(): void + { + $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN time("12:00:00.000000000") AS time')); + + $time = $results->first()->get('time'); + self::assertInstanceOf(Time::class, $time); + self::assertEquals(12.0 * 60 * 60 * 1_000_000_000, $time->getNanoSeconds()); + self::assertEquals(12.0 * 60 * 60 * 1_000_000_000, $time->nanoSeconds); + } + + public function testLocalTime(): void + { + $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN localtime("12") AS time')); + + /** @var LocalTime $time */ + $time = $results->first()->get('time'); + self::assertInstanceOf(LocalTime::class, $time); + self::assertEquals(43_200_000_000_000, $time->getNanoseconds()); + + $results = $this->getSession()->run('RETURN localtime("09:23:42.000") AS time', []); + + /** @var LocalTime $time */ + $time = $results->first()->get('time'); + self::assertInstanceOf(LocalTime::class, $time); + self::assertEquals(33_822_000_000_000, $time->getNanoseconds()); + self::assertEquals(33_822_000_000_000, $time->nanoseconds); + } + + public function testDateTime2(): void + { + $results = $this->getSession()->transaction(function (TransactionInterface $tsx) { + $query = $this->articlesQuery(); + $query .= 'RETURN article.created as created_at'; + + return $tsx->run($query); + }); + + self::assertEquals(3, $results->count()); + + $createdAt = $results[0]['created_at']; + self::assertInstanceOf(DateTime::class, $createdAt); + if ($createdAt->isLegacy()) { + self::assertEquals(1_559_414_432, $createdAt->getSeconds()); + } else { + self::assertEquals(1_559_410_832, $createdAt->getSeconds()); + } + + self::assertEquals(142_000_000, $createdAt->getNanoseconds()); + self::assertEquals(3600, $createdAt->getTimeZoneOffsetSeconds()); + self::assertEquals(142_000_000, $createdAt->getNanoseconds()); + self::assertEquals(3600, $createdAt->getTimeZoneOffsetSeconds()); + + if ($createdAt->isLegacy()) { + self::assertEquals('{"seconds":1559414432,"nanoseconds":142000000,"tzOffsetSeconds":3600}', json_encode($createdAt, JSON_THROW_ON_ERROR)); + } else { + self::assertEquals('{"seconds":1559410832,"nanoseconds":142000000,"tzOffsetSeconds":3600}', json_encode($createdAt, JSON_THROW_ON_ERROR)); + } + } + + public function testLocalDateTime(): void + { + $result = $this->getSession()->transaction(static fn (TransactionInterface $tsx): mixed => $tsx->run('RETURN localdatetime() as local')->first()->get('local')); + + self::assertInstanceOf(LocalDateTime::class, $result); + $date = $result->toDateTime(); + self::assertEquals($result->getSeconds(), $date->getTimestamp()); + } + + public function testDuration(): void + { + $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<count()); + self::assertEquals(new Duration(0, 14, 58320, 0), $results[0]['aDuration']); + $duration = $results[1]['aDuration']; + self::assertInstanceOf(Duration::class, $duration); + self::assertEquals(new Duration(5, 1, 43200, 0), $duration); + self::assertEquals(5, $duration->getMonths()); + self::assertEquals(1, $duration->getDays()); + self::assertEquals(43200, $duration->getSeconds()); + self::assertEquals(0, $duration->getNanoseconds()); + self::assertEquals(new Duration(0, 22, 71509, 500_000_000), $results[2]['aDuration']); + self::assertEquals(new Duration(0, 17, 43200, 0), $results[3]['aDuration']); + self::assertEquals(new Duration(0, 0, 91, 123_456_789), $results[4]['aDuration']); + self::assertEquals(new Duration(0, 0, 91, 123_456_789), $results[5]['aDuration']); + + self::assertEquals(5, $duration->getMonths()); + self::assertEquals(1, $duration->getDays()); + self::assertEquals(43200, $duration->getSeconds()); + self::assertEquals(0, $duration->getNanoseconds()); + $interval = new DateInterval(sprintf('P%dM%dDT%dS', 5, 1, 43200)); + self::assertEquals($interval, $duration->toDateInterval()); + self::assertEquals('{"months":5,"days":1,"seconds":43200,"nanoseconds":0}', json_encode($duration, JSON_THROW_ON_ERROR)); + } + + public function testPoint(): void + { + $result = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN point({x: 3, y: 4}) AS point')); + self::assertInstanceOf(CypherList::class, $result); + $row = $result->first(); + self::assertInstanceOf(CypherMap::class, $row); + $point = $row->get('point'); + + self::assertInstanceOf(CartesianPoint::class, $point); + self::assertEquals(3.0, $point->getX()); + self::assertEquals(4.0, $point->getY()); + self::assertEquals('cartesian', $point->getCrs()); + self::assertGreaterThan(0, $point->getSrid()); + self::assertEquals(3.0, $point->x); + self::assertEquals(4.0, $point->y); + self::assertEquals('cartesian', $point->crs); + self::assertGreaterThan(0, $point->srid); + self::assertEquals( + json_encode([ + 'x' => 3, + 'y' => 4, + 'crs' => 'cartesian', + 'srid' => 7203, + ], JSON_THROW_ON_ERROR), + json_encode($point, JSON_THROW_ON_ERROR) + ); + } + + public function testNode(): void + { + $uuid = 'cc60fd69-a92b-47f3-9674-2f27f3437d66'; + $email = 'a@b.c'; + $type = 'pepperoni'; + + $arguments = compact('email', 'uuid', 'type'); + + $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run( + 'MERGE (u:User{email: $email})-[:LIKES]->(p:Food:Pizza {type: $type}) ON CREATE SET u.uuid=$uuid RETURN u, p', + $arguments + )); + + self::assertEquals(1, $results->count()); + + /** @var Node $u */ + $u = $results[0]['u']; + self::assertInstanceOf(Node::class, $u); + self::assertEquals(['User'], $u->getLabels()->toArray()); + self::assertEquals($email, $u->getProperties()['email']); + self::assertEquals($uuid, $u->getProperties()['uuid']); + self::assertEquals($email, $u->email); + self::assertEquals($uuid, $u->uuid); + self::assertEquals( + json_encode([ + 'id' => $u->getId(), + 'labels' => $u->getLabels()->jsonSerialize(), + 'properties' => $u->getProperties()->jsonSerialize(), + ], JSON_THROW_ON_ERROR), + json_encode($u, JSON_THROW_ON_ERROR)); + + /** @var Node $p */ + $p = $results[0]['p']; + self::assertInstanceOf(Node::class, $p); + self::assertEquals(['Food', 'Pizza'], $p->getLabels()->toArray()); + self::assertEquals($type, $p->getProperties()['type']); + self::assertEquals( + json_encode([ + 'id' => $p->getId(), + 'labels' => $p->getLabels()->jsonSerialize(), + 'properties' => $p->getProperties()->jsonSerialize(), + ], JSON_THROW_ON_ERROR), + json_encode($p, JSON_THROW_ON_ERROR) + ); + } + + public function testRelationship(): void + { + $result = $this->getSession()->transaction(static function (TransactionInterface $tsx): mixed { + $tsx->run('MATCH (n) DETACH DELETE n'); + + return $tsx->run('MERGE (x:X {x: 1}) - [xy:XY {x: 1, y: 1}] -> (y:Y {y: 1}) RETURN xy')->first()->get('xy'); + }); + + self::assertInstanceOf(Relationship::class, $result); + self::assertEquals('XY', $result->getType()); + self::assertEquals(['x' => 1, 'y' => 1], $result->getProperties()->toArray()); + self::assertEquals(1, $result->x); + self::assertEquals(1, $result->y); + self::assertEquals( + json_encode([ + 'id' => $result->getId(), + 'type' => $result->getType(), + 'properties' => $result->getProperties(), + 'startNodeId' => $result->getStartNodeId(), + 'endNodeId' => $result->getEndNodeId(), + ], JSON_THROW_ON_ERROR), + json_encode($result, JSON_THROW_ON_ERROR) + ); + } + + public function testPath(): void + { + $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<<'CYPHER' +MERGE (b:Node {x:$x}) - [:HasNode {attribute: $xy}] -> (:Node {y:$y}) - [:HasNode {attribute: $yz}] -> (:Node {z:$z}) +WITH b +MATCH (x:Node) - [y:HasNode*2] -> (z:Node) +RETURN x, y, z +CYPHER, ['x' => 'x', 'xy' => 'xy', 'y' => 'y', 'yz' => 'yz', 'z' => 'z'])); + + self::assertEquals(1, $results->count()); + } + + 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}) +RETURN path +CYPHER, ['x' => 'x', 'xy' => 'xy', 'y' => 'y', 'yz' => 'yz', 'z' => 'z'])); + + self::assertEquals(1, $results->count()); + $path = $results->first()->get('path'); + + self::assertInstanceOf(Path::class, $path); + self::assertCount(2, $path->getRelationships()); + self::assertCount(3, $path->getNodes()); + + self::assertEquals(['x' => 'x'], $path->getNodes()->get(0)->getProperties()->toArray()); + self::assertEquals(['y' => 'y'], $path->getNodes()->get(1)->getProperties()->toArray()); + self::assertEquals(['z' => 'z'], $path->getNodes()->get(2)->getProperties()->toArray()); + self::assertEquals(['attribute' => 'xy'], $path->getRelationships()->get(0)->getProperties()->toArray()); + self::assertEquals(['attribute' => 'yz'], $path->getRelationships()->get(1)->getProperties()->toArray()); + } + + public function testPropertyTypes(): void + { + $result = $this->getSession(['neo4j', 'bolt'])->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<first()->get('a'); + + self::assertInstanceOf(Node::class, $node); + self::assertInstanceOf(PointInterface::class, $node->thePoint); + self::assertInstanceOf(CypherList::class, $node->theList); + self::assertInstanceOf(Date::class, $node->theDate); + self::assertInstanceOf(DateTime::class, $node->theDateTime); + self::assertInstanceOf(Duration::class, $node->theDuration); + self::assertInstanceOf(LocalDateTime::class, $node->theLocalDateTime); + self::assertInstanceOf(LocalTime::class, $node->theLocalTime); + self::assertInstanceOf(Time::class, $node->theTime); + } + + private function articlesQuery(): string + { + return << Date: Thu, 13 Feb 2025 12:13:49 +0530 Subject: [PATCH 09/10] reran php cs fixer --- .php-cs-fixer.dist.php | 1 - composer.json | 2 +- src/Authentication/BasicAuth.php | 3 +- src/Authentication/KerberosAuth.php | 4 +- src/Authentication/NoAuth.php | 6 +- src/Authentication/OpenIDConnectAuth.php | 6 +- src/Basic/Client.php | 5 +- src/Basic/Driver.php | 5 +- src/Basic/Session.php | 5 +- src/Basic/UnmanagedTransaction.php | 5 +- src/Bolt/BoltConnection.php | 9 ++- src/Bolt/BoltDriver.php | 5 +- src/Bolt/BoltResult.php | 5 +- src/Bolt/BoltUnmanagedTransaction.php | 5 +- src/Bolt/Connection.php | 5 +- src/Bolt/ConnectionPool.php | 7 +- src/Bolt/Session.php | 4 +- src/Bolt/SocketConnectionFactory.php | 5 +- src/Bolt/SystemWideConnectionFactory.php | 5 +- src/Bolt/UriConfiguration.php | 5 +- src/BoltFactory.php | 19 +++--- src/Client.php | 9 +-- src/ClientBuilder.php | 3 +- src/Common/Cache.php | 27 ++++---- src/Common/ConnectionConfiguration.php | 5 +- src/Common/DriverSetupManager.php | 5 +- src/Common/Neo4jLogger.php | 3 +- src/Common/SingleThreadedSemaphore.php | 5 +- src/Common/SysVSemaphore.php | 5 +- src/Common/Uri.php | 5 +- src/Databags/BookmarkHolder.php | 5 +- src/Databags/ConnectionRequestData.php | 5 +- src/Databags/DatabaseInfo.php | 5 +- src/Databags/DriverConfiguration.php | 12 ++-- src/Databags/DriverSetup.php | 5 +- src/Databags/InputPosition.php | 5 +- src/Databags/Neo4jError.php | 5 +- src/Databags/Notification.php | 5 +- src/Databags/Pair.php | 5 +- src/Databags/Plan.php | 5 +- src/Databags/ProfiledPlan.php | 5 +- src/Databags/ResultSummary.php | 5 +- src/Databags/ServerInfo.php | 5 +- src/Databags/SessionConfiguration.php | 11 +-- src/Databags/SslConfiguration.php | 5 +- src/Databags/Statement.php | 5 +- src/Databags/SummaryCounters.php | 5 +- src/Databags/TransactionConfiguration.php | 13 ++-- .../Specialised/BoltOGMTranslator.php | 3 +- src/Formatter/SummarizedResultFormatter.php | 5 +- src/Neo4j/Neo4jConnectionPool.php | 5 +- src/Neo4j/Neo4jDriver.php | 5 +- src/Neo4j/RoutingTable.php | 5 +- src/ParameterHelper.php | 6 +- src/TypeCaster.php | 1 - src/Types/Abstract3DPoint.php | 2 +- src/Types/AbstractPoint.php | 5 +- src/Types/ArrayList.php | 2 +- src/Types/CypherMap.php | 4 -- src/Types/Date.php | 5 +- src/Types/DateTime.php | 5 +- src/Types/DateTimeZoneId.php | 5 +- src/Types/Duration.php | 5 +- src/Types/LocalDateTime.php | 5 +- src/Types/LocalTime.php | 5 +- src/Types/Map.php | 9 +-- src/Types/Node.php | 5 +- src/Types/Path.php | 5 +- src/Types/Relationship.php | 2 +- src/Types/Time.php | 5 +- src/Types/UnboundRelationship.php | 5 +- .../SummarizedResultFormatterTest.php | 24 +++---- .../TransactionIntegrationTest.php | 68 +++++++++---------- tests/Unit/CypherMapTest.php | 2 +- tests/Unit/ParameterHelperTest.php | 4 +- 75 files changed, 274 insertions(+), 222 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index c673ac50..344e0983 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -63,7 +63,6 @@ MultilineCommentOpeningClosingAloneFixer::name() => true, MultilinePromotedPropertiesFixer::name() => true, PhpUnitAssertArgumentsOrderFixer::name() => true, - PhpdocNoSuperfluousParamFixer::name() => true, PhpdocParamOrderFixer::name() => true, StringableInterfaceFixer::name() => true, ]) diff --git a/composer.json b/composer.json index ffbc9954..be6b2da8 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "nyholm/psr7-server": "^1.0", "kriswallsmith/buzz": "^1.2", "vimeo/psalm": "^5.0", - "friendsofphp/php-cs-fixer": "3.15.0", + "friendsofphp/php-cs-fixer": "3.68.5", "psalm/plugin-phpunit": "^0.18", "monolog/monolog": "^2.2", "symfony/uid": "^5.0", diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php index 7f6fb256..82ef2d65 100644 --- a/src/Authentication/BasicAuth.php +++ b/src/Authentication/BasicAuth.php @@ -38,7 +38,8 @@ public function __construct( private readonly string $username, private readonly string $password, private readonly ?Neo4jLogger $logger, - ) {} + ) { + } /** * @throws Exception diff --git a/src/Authentication/KerberosAuth.php b/src/Authentication/KerberosAuth.php index 3c57d32f..51656d65 100644 --- a/src/Authentication/KerberosAuth.php +++ b/src/Authentication/KerberosAuth.php @@ -40,11 +40,13 @@ final class KerberosAuth implements AuthenticateInterface public function __construct( private readonly string $token, private readonly ?Neo4jLogger $logger, - ) {} + ) { + } public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface { $this->logger?->log(LogLevel::DEBUG, 'Authenticating using KerberosAuth'); + /** * @psalm-suppress ImpureMethodCall Request is a pure object: * diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php index 51d246cb..69d7ad07 100644 --- a/src/Authentication/NoAuth.php +++ b/src/Authentication/NoAuth.php @@ -38,12 +38,14 @@ final class NoAuth implements AuthenticateInterface * @pure */ public function __construct( - private readonly ?Neo4jLogger $logger - ) {} + private readonly ?Neo4jLogger $logger, + ) { + } public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface { $this->logger?->log(LogLevel::DEBUG, 'Authentication disabled'); + /** * @psalm-suppress ImpureMethodCall Request is a pure object: * diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php index 18b2b796..c7421cbf 100644 --- a/src/Authentication/OpenIDConnectAuth.php +++ b/src/Authentication/OpenIDConnectAuth.php @@ -36,12 +36,14 @@ final class OpenIDConnectAuth implements AuthenticateInterface */ public function __construct( private readonly string $token, - private readonly ?Neo4jLogger $logger - ) {} + private readonly ?Neo4jLogger $logger, + ) { + } public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface { $this->logger?->log(LogLevel::DEBUG, 'Authenticating using OpenIDConnectAuth'); + /** * @psalm-suppress ImpureMethodCall Request is a pure object: * diff --git a/src/Basic/Client.php b/src/Basic/Client.php index d6df61b4..0264cebc 100644 --- a/src/Basic/Client.php +++ b/src/Basic/Client.php @@ -22,8 +22,9 @@ final class Client implements ClientInterface { public function __construct( - private readonly ClientInterface $client - ) {} + private readonly ClientInterface $client, + ) { + } public function run(string $statement, iterable $parameters = [], ?string $alias = null): SummarizedResult { diff --git a/src/Basic/Driver.php b/src/Basic/Driver.php index 11baad9c..937949d9 100644 --- a/src/Basic/Driver.php +++ b/src/Basic/Driver.php @@ -27,8 +27,9 @@ final class Driver implements DriverInterface * @psalm-external-mutation-free */ public function __construct( - private readonly DriverInterface $driver - ) {} + private readonly DriverInterface $driver, + ) { + } /** * @psalm-mutation-free diff --git a/src/Basic/Session.php b/src/Basic/Session.php index bdb9015f..e417055e 100644 --- a/src/Basic/Session.php +++ b/src/Basic/Session.php @@ -23,8 +23,9 @@ final class Session implements SessionInterface { public function __construct( - private readonly SessionInterface $session - ) {} + private readonly SessionInterface $session, + ) { + } /** * @param iterable $statements diff --git a/src/Basic/UnmanagedTransaction.php b/src/Basic/UnmanagedTransaction.php index 03a4631d..d30f820e 100644 --- a/src/Basic/UnmanagedTransaction.php +++ b/src/Basic/UnmanagedTransaction.php @@ -21,8 +21,9 @@ final class UnmanagedTransaction implements UnmanagedTransactionInterface { public function __construct( - private readonly UnmanagedTransactionInterface $tsx - ) {} + private readonly UnmanagedTransactionInterface $tsx, + ) { + } /** * @param iterable $parameters diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 40710426..07a41a48 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -37,6 +37,7 @@ use Laudis\Neo4j\Types\CypherList; use Psr\Http\Message\UriInterface; use Psr\Log\LogLevel; +use Throwable; use WeakReference; /** @@ -80,7 +81,8 @@ public function __construct( /** @psalm-readonly */ private readonly ConnectionConfiguration $config, private readonly ?Neo4jLogger $logger, - ) {} + ) { + } public function getEncryptionLevel(): string { @@ -247,7 +249,7 @@ public function run( ?string $database, ?float $timeout, BookmarkHolder $holder, - ?AccessMode $mode + ?AccessMode $mode, ): array { $extra = $this->buildRunExtra($database, $timeout, $holder, $mode); $this->logger?->log(LogLevel::DEBUG, 'RUN', $extra); @@ -255,6 +257,7 @@ public function run( ->run($text, $parameters, $extra) ->getResponse(); $this->assertNoFailure($response); + /** @var BoltMeta */ return $response->content; } @@ -341,7 +344,7 @@ public function close(): void 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) { } } diff --git a/src/Bolt/BoltDriver.php b/src/Bolt/BoltDriver.php index a3625a2f..c8eb3d91 100644 --- a/src/Bolt/BoltDriver.php +++ b/src/Bolt/BoltDriver.php @@ -43,8 +43,9 @@ final class BoltDriver implements DriverInterface public function __construct( private readonly UriInterface $parsedUrl, private readonly ConnectionPool $pool, - private readonly SummarizedResultFormatter $formatter - ) {} + private readonly SummarizedResultFormatter $formatter, + ) { + } /** * @psalm-suppress MixedReturnTypeCoercion diff --git a/src/Bolt/BoltResult.php b/src/Bolt/BoltResult.php index e26f4126..f76676db 100644 --- a/src/Bolt/BoltResult.php +++ b/src/Bolt/BoltResult.php @@ -39,8 +39,9 @@ final class BoltResult implements Iterator public function __construct( private readonly BoltConnection $connection, private readonly int $fetchSize, - private readonly int $qid - ) {} + private readonly int $qid, + ) { + } public function getFetchSize(): int { diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index 74797dcf..908552fd 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -50,8 +50,9 @@ public function __construct( private readonly BoltConnection $connection, private readonly SessionConfiguration $config, private readonly TransactionConfiguration $tsxConfig, - private readonly BookmarkHolder $bookmarkHolder - ) {} + private readonly BookmarkHolder $bookmarkHolder, + ) { + } /** * @throws ClientException|Throwable diff --git a/src/Bolt/Connection.php b/src/Bolt/Connection.php index f4171649..e1145af0 100644 --- a/src/Bolt/Connection.php +++ b/src/Bolt/Connection.php @@ -22,8 +22,9 @@ class Connection */ public function __construct( private readonly IConnection $connection, - private readonly string $ssl - ) {} + private readonly string $ssl, + ) { + } public function getIConnection(): IConnection { diff --git a/src/Bolt/ConnectionPool.php b/src/Bolt/ConnectionPool.php index 2b2bfa25..33bf5e97 100644 --- a/src/Bolt/ConnectionPool.php +++ b/src/Bolt/ConnectionPool.php @@ -43,14 +43,15 @@ public function __construct( private readonly SemaphoreInterface $semaphore, private readonly BoltFactory $factory, private readonly ConnectionRequestData $data, - private readonly ?Neo4jLogger $logger - ) {} + private readonly ?Neo4jLogger $logger, + ) { + } public static function create( UriInterface $uri, AuthenticateInterface $auth, DriverConfiguration $conf, - SemaphoreInterface $semaphore + SemaphoreInterface $semaphore, ): self { return new self( $semaphore, diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php index 137a44c3..5f4818a0 100644 --- a/src/Bolt/Session.php +++ b/src/Bolt/Session.php @@ -54,7 +54,7 @@ public function __construct( /** * @psalm-readonly */ - private readonly SummarizedResultFormatter $formatter + private readonly SummarizedResultFormatter $formatter, ) { $this->bookmarkHolder = new BookmarkHolder(Bookmark::from($config->getBookmarks())); } @@ -136,7 +136,7 @@ public function beginTransaction(?iterable $statements = null, ?TransactionConfi */ private function beginInstantTransaction( SessionConfiguration $config, - TransactionConfiguration $tsxConfig + TransactionConfiguration $tsxConfig, ): TransactionInterface { $this->getLogger()?->log(LogLevel::INFO, 'Starting instant transaction', ['config' => $tsxConfig]); $connection = $this->acquireConnection($tsxConfig, $config); diff --git a/src/Bolt/SocketConnectionFactory.php b/src/Bolt/SocketConnectionFactory.php index 482d885a..0d2a97d7 100644 --- a/src/Bolt/SocketConnectionFactory.php +++ b/src/Bolt/SocketConnectionFactory.php @@ -20,8 +20,9 @@ final class SocketConnectionFactory implements BasicConnectionFactoryInterface { public function __construct( - private readonly StreamConnectionFactory $factory - ) {} + private readonly StreamConnectionFactory $factory, + ) { + } public function create(UriConfiguration $config): Connection { diff --git a/src/Bolt/SystemWideConnectionFactory.php b/src/Bolt/SystemWideConnectionFactory.php index 893be0a5..fe2c1598 100644 --- a/src/Bolt/SystemWideConnectionFactory.php +++ b/src/Bolt/SystemWideConnectionFactory.php @@ -28,8 +28,9 @@ class SystemWideConnectionFactory implements BasicConnectionFactoryInterface * @param SocketConnectionFactory|StreamConnectionFactory $factory */ private function __construct( - private $factory - ) {} + private $factory, + ) { + } /** * @psalm-suppress InvalidNullableReturnType diff --git a/src/Bolt/UriConfiguration.php b/src/Bolt/UriConfiguration.php index 225449b5..c61c4b6d 100644 --- a/src/Bolt/UriConfiguration.php +++ b/src/Bolt/UriConfiguration.php @@ -23,8 +23,9 @@ public function __construct( private readonly ?int $port, private readonly string $sslLevel, private readonly array $sslConfiguration, - private readonly ?float $timeout - ) {} + private readonly ?float $timeout, + ) { + } public function getHost(): string { diff --git a/src/BoltFactory.php b/src/BoltFactory.php index bd0e555d..1d50d3c2 100644 --- a/src/BoltFactory.php +++ b/src/BoltFactory.php @@ -42,8 +42,9 @@ public function __construct( private readonly BasicConnectionFactoryInterface $connectionFactory, private readonly ProtocolFactory $protocolFactory, private readonly SslConfigurationFactory $sslConfigurationFactory, - private readonly ?Neo4jLogger $logger = null - ) {} + private readonly ?Neo4jLogger $logger = null, + ) { + } public static function create(?Neo4jLogger $logger): self { @@ -87,13 +88,13 @@ public function canReuseConnection(ConnectionInterface $connection, ConnectionRe $databaseInfo = $connection->getDatabaseInfo(); $database = $databaseInfo?->getName(); - return $connection->getServerAddress()->getHost() === $data->getUri()->getHost() && - $connection->getServerAddress()->getPort() === $data->getUri()->getPort() && - $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() && - $database === $config->getDatabase(); + return $connection->getServerAddress()->getHost() === $data->getUri()->getHost() + && $connection->getServerAddress()->getPort() === $data->getUri()->getPort() + && $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() + && $database === $config->getDatabase(); } public function reuseConnection(BoltConnection $connection, SessionConfiguration $sessionConfig): BoltConnection diff --git a/src/Client.php b/src/Client.php index d055617c..4a114f66 100644 --- a/src/Client.php +++ b/src/Client.php @@ -47,8 +47,9 @@ final class Client implements ClientInterface public function __construct( private readonly DriverSetupManager $driverSetups, private readonly SessionConfiguration $defaultSessionConfiguration, - private readonly TransactionConfiguration $defaultTransactionConfiguration - ) {} + private readonly TransactionConfiguration $defaultTransactionConfiguration, + ) { + } public function getDriverSetups(): DriverSetupManager { @@ -79,8 +80,8 @@ private function getRunner(?string $alias = null): TransactionInterface|SessionI { $alias ??= $this->driverSetups->getDefaultAlias(); - if (array_key_exists($alias, $this->boundTransactions) && - count($this->boundTransactions[$alias]) > 0) { + if (array_key_exists($alias, $this->boundTransactions) + && count($this->boundTransactions[$alias]) > 0) { /** @psalm-suppress PossiblyNullArrayOffset */ return $this->boundTransactions[$alias][array_key_last($this->boundTransactions[$alias])]; } diff --git a/src/ClientBuilder.php b/src/ClientBuilder.php index a562480a..42a79232 100644 --- a/src/ClientBuilder.php +++ b/src/ClientBuilder.php @@ -46,7 +46,8 @@ public function __construct( /** @psalm-readonly */ private TransactionConfiguration $defaultTransactionConfig, private DriverSetupManager $driverSetups, - ) {} + ) { + } /** * Creates a client builder with default configurations and an OGMFormatter. diff --git a/src/Common/Cache.php b/src/Common/Cache.php index 5f37089d..9b225e78 100644 --- a/src/Common/Cache.php +++ b/src/Common/Cache.php @@ -47,7 +47,9 @@ class Cache implements CacheInterface private array $items = []; private static ?self $instance = null; - private function __construct() {} + private function __construct() + { + } public static function getInstance(): self { @@ -158,12 +160,11 @@ public function getMultiple($keys, $default = null): Generator } /** - * @param iterable $values - * @param int|DateInterval|null $ttl + * @param iterable $values * * @throws InvalidCacheArgumentException */ - public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool + public function setMultiple(iterable $values, int|DateInterval|null $ttl = null): bool { /** * @var mixed $key @@ -185,15 +186,15 @@ public function setMultiple(iterable $values, null|int|DateInterval $ttl = null) */ private function assertValidKey(string $key): void { - if ($key === '' || - str_contains($key, '{') || - str_contains($key, '}') || - str_contains($key, '(') || - str_contains($key, ')') || - str_contains($key, '/') || - str_contains($key, '\\') || - str_contains($key, '@') || - str_contains($key, ':') + if ($key === '' + || str_contains($key, '{') + || str_contains($key, '}') + || str_contains($key, '(') + || str_contains($key, ')') + || str_contains($key, '/') + || str_contains($key, '\\') + || str_contains($key, '@') + || str_contains($key, ':') ) { throw new InvalidCacheArgumentException(); } diff --git a/src/Common/ConnectionConfiguration.php b/src/Common/ConnectionConfiguration.php index fe791c34..3d69fa43 100644 --- a/src/Common/ConnectionConfiguration.php +++ b/src/Common/ConnectionConfiguration.php @@ -33,8 +33,9 @@ public function __construct( private readonly ConnectionProtocol $protocol, private readonly AccessMode $accessMode, private readonly ?DatabaseInfo $databaseInfo, - private readonly string $encryptionLevel - ) {} + private readonly string $encryptionLevel, + ) { + } public function getServerAgent(): string { diff --git a/src/Common/DriverSetupManager.php b/src/Common/DriverSetupManager.php index 28b5646b..fdc7c66f 100644 --- a/src/Common/DriverSetupManager.php +++ b/src/Common/DriverSetupManager.php @@ -51,8 +51,9 @@ class DriverSetupManager implements Countable */ public function __construct( private SummarizedResultFormatter $formatter, - private DriverConfiguration $configuration - ) {} + private DriverConfiguration $configuration, + ) { + } public function getDriverConfiguration(): DriverConfiguration { diff --git a/src/Common/Neo4jLogger.php b/src/Common/Neo4jLogger.php index 60276411..40aab4df 100644 --- a/src/Common/Neo4jLogger.php +++ b/src/Common/Neo4jLogger.php @@ -33,7 +33,8 @@ class Neo4jLogger public function __construct( private readonly string $level, private readonly ?LoggerInterface $logger, - ) {} + ) { + } public function log(string $level, string $message, array $context = []): void { diff --git a/src/Common/SingleThreadedSemaphore.php b/src/Common/SingleThreadedSemaphore.php index ada6f4a8..77c8dbe5 100644 --- a/src/Common/SingleThreadedSemaphore.php +++ b/src/Common/SingleThreadedSemaphore.php @@ -27,8 +27,9 @@ class SingleThreadedSemaphore implements SemaphoreInterface private static array $instances = []; private function __construct( - private readonly int $max - ) {} + private readonly int $max, + ) { + } public static function create(string $key, int $max): self { diff --git a/src/Common/SysVSemaphore.php b/src/Common/SysVSemaphore.php index 0845fa21..c3d872f2 100644 --- a/src/Common/SysVSemaphore.php +++ b/src/Common/SysVSemaphore.php @@ -30,8 +30,9 @@ class SysVSemaphore implements SemaphoreInterface { private function __construct( - private readonly \SysvSemaphore $semaphore - ) {} + private readonly \SysvSemaphore $semaphore, + ) { + } public static function create(string $key, int $max): self { diff --git a/src/Common/Uri.php b/src/Common/Uri.php index 6d2393b5..59275a42 100644 --- a/src/Common/Uri.php +++ b/src/Common/Uri.php @@ -38,8 +38,9 @@ public function __construct( private readonly ?int $port, private string $path, private readonly string $query, - private readonly string $fragment - ) {} + private readonly string $fragment, + ) { + } /** * @pure diff --git a/src/Databags/BookmarkHolder.php b/src/Databags/BookmarkHolder.php index 48109d19..9789a054 100644 --- a/src/Databags/BookmarkHolder.php +++ b/src/Databags/BookmarkHolder.php @@ -16,8 +16,9 @@ final class BookmarkHolder { public function __construct( - private Bookmark $bookmark - ) {} + private Bookmark $bookmark, + ) { + } public function getBookmark(): Bookmark { diff --git a/src/Databags/ConnectionRequestData.php b/src/Databags/ConnectionRequestData.php index b5772b1a..764f8573 100644 --- a/src/Databags/ConnectionRequestData.php +++ b/src/Databags/ConnectionRequestData.php @@ -26,8 +26,9 @@ public function __construct( private readonly UriInterface $uri, private readonly AuthenticateInterface $auth, private readonly string $userAgent, - private readonly SslConfiguration $config - ) {} + private readonly SslConfiguration $config, + ) { + } public function getHostname(): string { diff --git a/src/Databags/DatabaseInfo.php b/src/Databags/DatabaseInfo.php index 2f88cab0..0cd90e15 100644 --- a/src/Databags/DatabaseInfo.php +++ b/src/Databags/DatabaseInfo.php @@ -25,8 +25,9 @@ final class DatabaseInfo extends AbstractCypherObject { public function __construct( - private readonly string $name - ) {} + private readonly string $name, + ) { + } /** * Returns the name of the database. diff --git a/src/Databags/DriverConfiguration.php b/src/Databags/DriverConfiguration.php index edbd355d..68c35a87 100644 --- a/src/Databags/DriverConfiguration.php +++ b/src/Databags/DriverConfiguration.php @@ -46,21 +46,21 @@ final class DriverConfiguration private ?Neo4jLogger $logger; /** - * @param callable():(CacheInterface|null)|CacheInterface|null $cache + * @param callable():(CacheInterface|null)|CacheInterface|null $cache * @param callable():(SemaphoreFactoryInterface|null)|SemaphoreFactoryInterface|null $semaphore - * @param string|null $logLevel The log level to use. If null, LogLevel::INFO is used. + * @param string|null $logLevel The log level to use. If null, LogLevel::INFO is used. * * @psalm-external-mutation-free */ public function __construct( - private string|null $userAgent, + private ?string $userAgent, private SslConfiguration $sslConfig, - private int|null $maxPoolSize, + private ?int $maxPoolSize, CacheInterface|callable|null $cache, - private float|null $acquireConnectionTimeout, + private ?float $acquireConnectionTimeout, callable|SemaphoreFactoryInterface|null $semaphore, ?string $logLevel, - ?LoggerInterface $logger + ?LoggerInterface $logger, ) { $this->cache = $cache; $this->semaphoreFactory = $semaphore; diff --git a/src/Databags/DriverSetup.php b/src/Databags/DriverSetup.php index 67066fc4..00317c06 100644 --- a/src/Databags/DriverSetup.php +++ b/src/Databags/DriverSetup.php @@ -25,8 +25,9 @@ final class DriverSetup { public function __construct( private readonly UriInterface $uri, - private readonly AuthenticateInterface $auth - ) {} + private readonly AuthenticateInterface $auth, + ) { + } public function getAuth(): AuthenticateInterface { diff --git a/src/Databags/InputPosition.php b/src/Databags/InputPosition.php index 7e81d118..ca548925 100644 --- a/src/Databags/InputPosition.php +++ b/src/Databags/InputPosition.php @@ -23,8 +23,9 @@ final class InputPosition public function __construct( private readonly int $column, private readonly int $line, - private readonly int $offset - ) {} + private readonly int $offset, + ) { + } /** * The column number referred to by the position; column numbers start at 1. diff --git a/src/Databags/Neo4jError.php b/src/Databags/Neo4jError.php index d592ebed..dab5f788 100644 --- a/src/Databags/Neo4jError.php +++ b/src/Databags/Neo4jError.php @@ -30,8 +30,9 @@ public function __construct( private readonly ?string $message, private readonly string $classification, private readonly string $category, - private readonly string $title - ) {} + private readonly string $title, + ) { + } /** * @pure diff --git a/src/Databags/Notification.php b/src/Databags/Notification.php index c372ccba..f11daf40 100644 --- a/src/Databags/Notification.php +++ b/src/Databags/Notification.php @@ -25,8 +25,9 @@ public function __construct( private readonly string $description, private readonly ?InputPosition $inputPosition, private readonly string $severity, - private readonly string $title - ) {} + private readonly string $title, + ) { + } /** * Returns a notification code for the discovered issue. diff --git a/src/Databags/Pair.php b/src/Databags/Pair.php index 05585b39..247a9383 100644 --- a/src/Databags/Pair.php +++ b/src/Databags/Pair.php @@ -29,8 +29,9 @@ final class Pair */ public function __construct( private $key, - private $value - ) {} + private $value, + ) { + } /** * @return TKey diff --git a/src/Databags/Plan.php b/src/Databags/Plan.php index 024653b7..76a317c0 100644 --- a/src/Databags/Plan.php +++ b/src/Databags/Plan.php @@ -37,8 +37,9 @@ public function __construct( private readonly CypherMap $arguments, private readonly CypherList $list, private readonly CypherList $identifiers, - private readonly string $operator - ) {} + private readonly string $operator, + ) { + } /** * Returns the arguments for the operator. diff --git a/src/Databags/ProfiledPlan.php b/src/Databags/ProfiledPlan.php index 045cd25d..64c325d4 100644 --- a/src/Databags/ProfiledPlan.php +++ b/src/Databags/ProfiledPlan.php @@ -38,8 +38,9 @@ public function __construct( private readonly int $pageCacheHits, private readonly int $pageCacheMisses, private readonly int $records, - private readonly int $time - ) {} + private readonly int $time, + ) { + } /** * @return CypherList diff --git a/src/Databags/ResultSummary.php b/src/Databags/ResultSummary.php index 6992ecda..a5aa05f8 100644 --- a/src/Databags/ResultSummary.php +++ b/src/Databags/ResultSummary.php @@ -46,8 +46,9 @@ public function __construct( private readonly QueryTypeEnum $queryType, private readonly float $resultAvailableAfter, private readonly float $resultConsumedAfter, - private readonly ServerInfo $serverInfo - ) {} + private readonly ServerInfo $serverInfo, + ) { + } /** * The counters for amount of operations the query triggered. diff --git a/src/Databags/ServerInfo.php b/src/Databags/ServerInfo.php index 7408103c..ce8fa011 100644 --- a/src/Databags/ServerInfo.php +++ b/src/Databags/ServerInfo.php @@ -29,8 +29,9 @@ final class ServerInfo extends AbstractCypherObject public function __construct( private readonly UriInterface $address, private readonly ConnectionProtocol $protocol, - private readonly string $agent - ) {} + private readonly string $agent, + ) { + } /** * Returns the uri of the server the query was executed. diff --git a/src/Databags/SessionConfiguration.php b/src/Databags/SessionConfiguration.php index 56adcdc9..397e864a 100644 --- a/src/Databags/SessionConfiguration.php +++ b/src/Databags/SessionConfiguration.php @@ -37,18 +37,19 @@ final class SessionConfiguration */ public function __construct( private readonly ?string $database = null, - private readonly int|null $fetchSize = null, - private readonly AccessMode|null $accessMode = null, - private readonly array|null $bookmarks = null, + private readonly ?int $fetchSize = null, + private readonly ?AccessMode $accessMode = null, + private readonly ?array $bookmarks = null, private readonly ?Neo4jLogger $logger = null, - ) {} + ) { + } /** * @pure * * @param list|null $bookmarks */ - public static function create(string|null $database = null, int|null $fetchSize = null, AccessMode|null $defaultAccessMode = null, array|null $bookmarks = null, ?Neo4jLogger $logger = null): self + public static function create(?string $database = null, ?int $fetchSize = null, ?AccessMode $defaultAccessMode = null, ?array $bookmarks = null, ?Neo4jLogger $logger = null): self { return new self($database, $fetchSize, $defaultAccessMode, $bookmarks, $logger); } diff --git a/src/Databags/SslConfiguration.php b/src/Databags/SslConfiguration.php index 58741c77..07fd5c4e 100644 --- a/src/Databags/SslConfiguration.php +++ b/src/Databags/SslConfiguration.php @@ -22,8 +22,9 @@ final class SslConfiguration { public function __construct( private SslMode $mode, - private bool $verifyPeer - ) {} + private bool $verifyPeer, + ) { + } public function getMode(): SslMode { diff --git a/src/Databags/Statement.php b/src/Databags/Statement.php index f3cc3731..a7ddd8a4 100644 --- a/src/Databags/Statement.php +++ b/src/Databags/Statement.php @@ -31,8 +31,9 @@ final class Statement extends AbstractCypherObject */ public function __construct( private readonly string $text, - private readonly iterable $parameters - ) {} + private readonly iterable $parameters, + ) { + } /** * @pure diff --git a/src/Databags/SummaryCounters.php b/src/Databags/SummaryCounters.php index 6df81975..9fb4efbc 100644 --- a/src/Databags/SummaryCounters.php +++ b/src/Databags/SummaryCounters.php @@ -38,8 +38,9 @@ public function __construct( private readonly int $constraintsRemoved = 0, private readonly bool $containsUpdates = false, private readonly bool $containsSystemUpdates = false, - private readonly int $systemUpdates = 0 - ) {} + private readonly int $systemUpdates = 0, + ) { + } /** * Whether the query contained any updates. diff --git a/src/Databags/TransactionConfiguration.php b/src/Databags/TransactionConfiguration.php index c1dacf0e..bc9a68d9 100644 --- a/src/Databags/TransactionConfiguration.php +++ b/src/Databags/TransactionConfiguration.php @@ -28,9 +28,10 @@ final class TransactionConfiguration * @param iterable|null $metaData */ public function __construct( - private float|null $timeout = null, - private iterable|null $metaData = null - ) {} + private ?float $timeout = null, + private ?iterable $metaData = null, + ) { + } /** * @pure @@ -38,7 +39,7 @@ public function __construct( * @param float|null $timeout timeout in seconds * @param iterable|null $metaData */ - public static function create(float|null $timeout = null, iterable|null $metaData = null): self + public static function create(?float $timeout = null, ?iterable $metaData = null): self { return new self($timeout, $metaData); } @@ -74,7 +75,7 @@ public function getTimeout(): ?float * * @param float|null $timeout timeout in seconds */ - public function withTimeout(float|null $timeout): self + public function withTimeout(?float $timeout): self { return new self($timeout, $this->metaData); } @@ -84,7 +85,7 @@ public function withTimeout(float|null $timeout): self * * @param iterable|null $metaData */ - public function withMetaData(iterable|null $metaData): self + public function withMetaData(?iterable $metaData): self { return new self($this->timeout, $metaData); } diff --git a/src/Formatter/Specialised/BoltOGMTranslator.php b/src/Formatter/Specialised/BoltOGMTranslator.php index 43213190..16165eab 100644 --- a/src/Formatter/Specialised/BoltOGMTranslator.php +++ b/src/Formatter/Specialised/BoltOGMTranslator.php @@ -107,6 +107,7 @@ private function makeFromBoltNode(BoltNode $node): Node if ($node instanceof \Bolt\protocol\v5\structures\Node) { $elementId = $node->element_id; } + /** * @psalm-suppress MixedArgumentTypeCoercion */ @@ -219,7 +220,7 @@ private function makeFromBoltUnboundRelationship(BoltUnboundRelationship $rel): ); } - private function makeFromBoltPoint2D(BoltPoint2d $x): AbstractPoint + private function makeFromBoltPoint2D(BoltPoint2D $x): AbstractPoint { if ($x->srid === CartesianPoint::SRID) { return new CartesianPoint($x->x, $x->y); diff --git a/src/Formatter/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php index 215e2d7f..789c5438 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -105,8 +105,9 @@ public static function create(): self * @psalm-mutation-free */ public function __construct( - private readonly BoltOGMTranslator $translator - ) {} + private readonly BoltOGMTranslator $translator, + ) { + } /** * @param array{stats?: BoltCypherStats}&array $response diff --git a/src/Neo4j/Neo4jConnectionPool.php b/src/Neo4j/Neo4jConnectionPool.php index e3a064be..e4ad758d 100644 --- a/src/Neo4j/Neo4jConnectionPool.php +++ b/src/Neo4j/Neo4jConnectionPool.php @@ -74,14 +74,15 @@ public function __construct( private readonly CacheInterface $cache, private readonly AddressResolverInterface $resolver, private readonly ?Neo4jLogger $logger, - ) {} + ) { + } public static function create( UriInterface $uri, AuthenticateInterface $auth, DriverConfiguration $conf, AddressResolverInterface $resolver, - SemaphoreInterface $semaphore + SemaphoreInterface $semaphore, ): self { return new self( $semaphore, diff --git a/src/Neo4j/Neo4jDriver.php b/src/Neo4j/Neo4jDriver.php index 768d8bf6..54bb1f5a 100644 --- a/src/Neo4j/Neo4jDriver.php +++ b/src/Neo4j/Neo4jDriver.php @@ -46,8 +46,9 @@ final class Neo4jDriver implements DriverInterface public function __construct( private readonly UriInterface $parsedUrl, private readonly Neo4jConnectionPool $pool, - private readonly SummarizedResultFormatter $formatter - ) {} + private readonly SummarizedResultFormatter $formatter, + ) { + } /** * @psalm-suppress MixedReturnTypeCoercion diff --git a/src/Neo4j/RoutingTable.php b/src/Neo4j/RoutingTable.php index 3b5b2b76..4e6d7539 100644 --- a/src/Neo4j/RoutingTable.php +++ b/src/Neo4j/RoutingTable.php @@ -29,8 +29,9 @@ final class RoutingTable */ public function __construct( private readonly iterable $servers, - private readonly int $ttl - ) {} + private readonly int $ttl, + ) { + } /** * Returns the time to live in seconds. diff --git a/src/ParameterHelper.php b/src/ParameterHelper.php index 0a064f27..83570a7f 100644 --- a/src/ParameterHelper.php +++ b/src/ParameterHelper.php @@ -84,7 +84,7 @@ public static function asMap(iterable $iterable): CypherMap */ public static function asParameter( mixed $value, - ConnectionProtocol $protocol + ConnectionProtocol $protocol, ): iterable|int|float|bool|string|stdClass|IStructure|null { return self::cypherMapToStdClass($value) ?? self::emptySequenceToArray($value) ?? @@ -124,8 +124,8 @@ private static function filterInvalidType(mixed $value): mixed private static function emptySequenceToArray(mixed $value): ?array { - if ((($value instanceof CypherList || $value instanceof CypherMap) && $value->count() === 0) || - (is_array($value) && count($value) === 0)) { + if ((($value instanceof CypherList || $value instanceof CypherMap) && $value->count() === 0) + || (is_array($value) && count($value) === 0)) { return []; } diff --git a/src/TypeCaster.php b/src/TypeCaster.php index cd0ecd41..053ca90c 100644 --- a/src/TypeCaster.php +++ b/src/TypeCaster.php @@ -117,7 +117,6 @@ public static function toArray(mixed $value): ?array $tbr = []; /** @var mixed $x */ foreach ($value as $x) { - /** @var mixed */ $tbr[] = $x; } diff --git a/src/Types/Abstract3DPoint.php b/src/Types/Abstract3DPoint.php index 9d645e70..320c84a2 100644 --- a/src/Types/Abstract3DPoint.php +++ b/src/Types/Abstract3DPoint.php @@ -37,7 +37,7 @@ public function convertToBolt(): IStructure public function __construct( float $x, float $y, - private float $z + private float $z, ) { parent::__construct($x, $y); } diff --git a/src/Types/AbstractPoint.php b/src/Types/AbstractPoint.php index ac5da8cf..90765c2f 100644 --- a/src/Types/AbstractPoint.php +++ b/src/Types/AbstractPoint.php @@ -33,8 +33,9 @@ abstract class AbstractPoint extends AbstractPropertyObject implements PointInte { public function __construct( private readonly float $x, - private readonly float $y - ) {} + private readonly float $y, + ) { + } abstract public function getCrs(): string; diff --git a/src/Types/ArrayList.php b/src/Types/ArrayList.php index b41fe92f..30e6747f 100644 --- a/src/Types/ArrayList.php +++ b/src/Types/ArrayList.php @@ -59,7 +59,7 @@ public function __construct($iterable = []) /** * @template Value * - * @param callable():(\Generator) $operation + * @param callable():(Generator) $operation * * @return static * diff --git a/src/Types/CypherMap.php b/src/Types/CypherMap.php index 5037299e..22426e80 100644 --- a/src/Types/CypherMap.php +++ b/src/Types/CypherMap.php @@ -33,10 +33,8 @@ final class CypherMap extends Map public function getAsCypherMap(string $key, mixed $default = null): CypherMap { if (func_num_args() === 1) { - /** @var mixed $value */ $value = $this->get($key); } else { - /** @var mixed $value */ $value = $this->get($key, $default); } $tbr = TypeCaster::toCypherMap($value); @@ -53,10 +51,8 @@ public function getAsCypherMap(string $key, mixed $default = null): CypherMap public function getAsCypherList(string $key, mixed $default = null): CypherList { if (func_num_args() === 1) { - /** @var mixed $value */ $value = $this->get($key); } else { - /** @var mixed $value */ $value = $this->get($key, $default); } $tbr = TypeCaster::toCypherList($value); diff --git a/src/Types/Date.php b/src/Types/Date.php index 070d5ec8..ed85650d 100644 --- a/src/Types/Date.php +++ b/src/Types/Date.php @@ -31,8 +31,9 @@ final class Date extends AbstractPropertyObject implements BoltConvertibleInterface { public function __construct( - private readonly int $days - ) {} + private readonly int $days, + ) { + } /** * The amount of days since unix epoch. diff --git a/src/Types/DateTime.php b/src/Types/DateTime.php index f4a1b421..6d565c0c 100644 --- a/src/Types/DateTime.php +++ b/src/Types/DateTime.php @@ -34,8 +34,9 @@ public function __construct( private readonly int $seconds, private readonly int $nanoseconds, private readonly int $tzOffsetSeconds, - private readonly bool $legacy - ) {} + private readonly bool $legacy, + ) { + } /** * Returns whether this DateTime Type follows conventions up until Neo4j version 4. diff --git a/src/Types/DateTimeZoneId.php b/src/Types/DateTimeZoneId.php index b734aebd..1ebf88a1 100644 --- a/src/Types/DateTimeZoneId.php +++ b/src/Types/DateTimeZoneId.php @@ -40,8 +40,9 @@ final class DateTimeZoneId extends AbstractPropertyObject implements BoltConvert public function __construct( private readonly int $seconds, private readonly int $nanoseconds, - private readonly string $tzId - ) {} + private readonly string $tzId, + ) { + } /** * Returns the amount of seconds since unix epoch. diff --git a/src/Types/Duration.php b/src/Types/Duration.php index cf0dd293..611468b6 100644 --- a/src/Types/Duration.php +++ b/src/Types/Duration.php @@ -31,8 +31,9 @@ public function __construct( private readonly int $months, private readonly int $days, private readonly int $seconds, - private readonly int $nanoseconds - ) {} + private readonly int $nanoseconds, + ) { + } /** * The amount of months in the duration. diff --git a/src/Types/LocalDateTime.php b/src/Types/LocalDateTime.php index 114d09b6..0c851fbe 100644 --- a/src/Types/LocalDateTime.php +++ b/src/Types/LocalDateTime.php @@ -35,8 +35,9 @@ final class LocalDateTime extends AbstractPropertyObject implements BoltConverti { public function __construct( private readonly int $seconds, - private readonly int $nanoseconds - ) {} + private readonly int $nanoseconds, + ) { + } /** * The amount of seconds since the unix epoch. diff --git a/src/Types/LocalTime.php b/src/Types/LocalTime.php index f53b8644..24a8ba05 100644 --- a/src/Types/LocalTime.php +++ b/src/Types/LocalTime.php @@ -26,8 +26,9 @@ final class LocalTime extends AbstractPropertyObject implements BoltConvertibleInterface { public function __construct( - private readonly int $nanoseconds - ) {} + private readonly int $nanoseconds, + ) { + } /** * The nanoseconds that have passed since midnight. diff --git a/src/Types/Map.php b/src/Types/Map.php index 2ca1c775..712f0b9d 100644 --- a/src/Types/Map.php +++ b/src/Types/Map.php @@ -89,7 +89,7 @@ public function __construct($iterable = []) /** * @template Value * - * @param callable():(\Generator) $operation + * @param callable():(Generator) $operation * * @return static * @@ -364,7 +364,6 @@ 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); @@ -380,7 +379,6 @@ 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); @@ -396,7 +394,6 @@ 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); @@ -412,7 +409,6 @@ 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); @@ -448,7 +444,6 @@ public function getAsObject(string $key, string $class, mixed $default = null): if (func_num_args() === 1) { $value = $this->get($key); } else { - /** @var mixed */ $value = $this->get($key, $default); } $tbr = TypeCaster::toClass($value, $class); @@ -467,7 +462,6 @@ 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); } @@ -486,7 +480,6 @@ 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)) { diff --git a/src/Types/Node.php b/src/Types/Node.php index 0c660714..4c529fab 100644 --- a/src/Types/Node.php +++ b/src/Types/Node.php @@ -39,8 +39,9 @@ public function __construct( private readonly int $id, private readonly CypherList $labels, private readonly CypherMap $properties, - private readonly ?string $elementId - ) {} + private readonly ?string $elementId, + ) { + } /** * The labels on the node. diff --git a/src/Types/Path.php b/src/Types/Path.php index 2e18a9c2..a52c77b3 100644 --- a/src/Types/Path.php +++ b/src/Types/Path.php @@ -30,8 +30,9 @@ final class Path extends AbstractPropertyObject public function __construct( private readonly CypherList $nodes, private readonly CypherList $relationships, - private readonly CypherList $ids - ) {} + private readonly CypherList $ids, + ) { + } /** * Returns the node in the path. diff --git a/src/Types/Relationship.php b/src/Types/Relationship.php index 0528d58d..f0b716a9 100644 --- a/src/Types/Relationship.php +++ b/src/Types/Relationship.php @@ -33,7 +33,7 @@ public function __construct( private readonly int $endNodeId, string $type, CypherMap $properties, - ?string $elementId + ?string $elementId, ) { parent::__construct($id, $type, $properties, $elementId); } diff --git a/src/Types/Time.php b/src/Types/Time.php index 626c16e7..54f87719 100644 --- a/src/Types/Time.php +++ b/src/Types/Time.php @@ -27,8 +27,9 @@ final class Time extends AbstractPropertyObject implements BoltConvertibleInterf { public function __construct( private readonly int $nanoSeconds, - private readonly int $tzOffsetSeconds - ) {} + private readonly int $tzOffsetSeconds, + ) { + } /** * @return array{nanoSeconds: int, tzOffsetSeconds: int} diff --git a/src/Types/UnboundRelationship.php b/src/Types/UnboundRelationship.php index 1148c960..cde5fdb1 100644 --- a/src/Types/UnboundRelationship.php +++ b/src/Types/UnboundRelationship.php @@ -36,8 +36,9 @@ public function __construct( private readonly int $id, private readonly string $type, private readonly CypherMap $properties, - private readonly ?string $elementId - ) {} + private readonly ?string $elementId, + ) { + } public function getElementId(): ?string { diff --git a/tests/Integration/SummarizedResultFormatterTest.php b/tests/Integration/SummarizedResultFormatterTest.php index 52b3468e..7aecb7e2 100644 --- a/tests/Integration/SummarizedResultFormatterTest.php +++ b/tests/Integration/SummarizedResultFormatterTest.php @@ -13,31 +13,31 @@ namespace Laudis\Neo4j\Tests\Integration; -use DateInterval; -use Laudis\Neo4j\Contracts\PointInterface; -use Laudis\Neo4j\Types\CartesianPoint; -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 function bin2hex; +use DateInterval; use DateTimeImmutable; use function dump; +use Laudis\Neo4j\Contracts\PointInterface; use Laudis\Neo4j\Contracts\TransactionInterface; use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\SummaryCounters; use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest; +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 function random_bytes; use function serialize; diff --git a/tests/Integration/TransactionIntegrationTest.php b/tests/Integration/TransactionIntegrationTest.php index e575129b..cbd56b2a 100644 --- a/tests/Integration/TransactionIntegrationTest.php +++ b/tests/Integration/TransactionIntegrationTest.php @@ -279,40 +279,40 @@ public function testRollbackInvalid(): void self::assertFalse($tsx->isCommitted()); } -// /** -// * TODO - rework this test -// * @dataProvider connectionAliases -// * @noinspection PhpUnusedLocalVariableInspection -// * @psalm-suppress UnusedVariable -// */ -// public function testCorrectConnectionReuse(): void -// { -// $driver = $this->getSession()->getDriver($alias); -// if (!$driver instanceof BoltDriver) { -// self::markTestSkipped('Can only white box test bolt driver'); -// } -// -// $poolReflection = new ReflectionClass(Connection::class); -// $poolReflection->setStaticPropertyValue('connectionCache', []); -// -// $this->getSession()->run('MATCH (x) RETURN x', []); -// $this->getSession()->run('MATCH (x) RETURN x', []); -// $this->getSession()->run('MATCH (x) RETURN x', []); -// $this->getSession()->run('MATCH (x) RETURN x', []); -// $a = $this->getSession()->beginTransaction([]); -// $b = $this->getSession()->beginTransaction([]); -// $this->getSession()->run('MATCH (x) RETURN x', []); -// -// $poolReflection = new ReflectionClass(ConnectionPool::class); -// /** @var array $cache */ -// $cache = $poolReflection->getStaticPropertyValue('connectionCache'); -// -// $key = array_key_first($cache); -// self::assertIsString($key); -// self::assertArrayHasKey($key, $cache); -// /** @psalm-suppress MixedArgument */ -// self::assertCount(3, $cache[$key]); -// } + // /** + // * TODO - rework this test + // * @dataProvider connectionAliases + // * @noinspection PhpUnusedLocalVariableInspection + // * @psalm-suppress UnusedVariable + // */ + // public function testCorrectConnectionReuse(): void + // { + // $driver = $this->getSession()->getDriver($alias); + // if (!$driver instanceof BoltDriver) { + // self::markTestSkipped('Can only white box test bolt driver'); + // } + // + // $poolReflection = new ReflectionClass(Connection::class); + // $poolReflection->setStaticPropertyValue('connectionCache', []); + // + // $this->getSession()->run('MATCH (x) RETURN x', []); + // $this->getSession()->run('MATCH (x) RETURN x', []); + // $this->getSession()->run('MATCH (x) RETURN x', []); + // $this->getSession()->run('MATCH (x) RETURN x', []); + // $a = $this->getSession()->beginTransaction([]); + // $b = $this->getSession()->beginTransaction([]); + // $this->getSession()->run('MATCH (x) RETURN x', []); + // + // $poolReflection = new ReflectionClass(ConnectionPool::class); + // /** @var array $cache */ + // $cache = $poolReflection->getStaticPropertyValue('connectionCache'); + // + // $key = array_key_first($cache); + // self::assertIsString($key); + // self::assertArrayHasKey($key, $cache); + // /** @psalm-suppress MixedArgument */ + // self::assertCount(3, $cache[$key]); + // } #[DoesNotPerformAssertions] public function testTransactionRunNoConsumeResult(): void diff --git a/tests/Unit/CypherMapTest.php b/tests/Unit/CypherMapTest.php index 848112b7..fc63025f 100644 --- a/tests/Unit/CypherMapTest.php +++ b/tests/Unit/CypherMapTest.php @@ -426,7 +426,7 @@ public function testSkipInvalid(): void public function testInvalidConstruct(): void { /** @psalm-suppress MissingTemplateParam */ - $map = new CypherMap(new class() implements IteratorAggregate { + $map = new CypherMap(new class implements IteratorAggregate { public function getIterator(): Generator { yield new stdClass() => 'x'; diff --git a/tests/Unit/ParameterHelperTest.php b/tests/Unit/ParameterHelperTest.php index 83ce18a0..b45e294b 100644 --- a/tests/Unit/ParameterHelperTest.php +++ b/tests/Unit/ParameterHelperTest.php @@ -36,7 +36,7 @@ public static function setUpBeforeClass(): void * @psalm-suppress MixedPropertyTypeCoercion * @psalm-suppress MissingTemplateParam */ - self::$invalidIterable = new class() implements Iterator { + self::$invalidIterable = new class implements Iterator { private bool $initial = true; public function current(): int @@ -144,7 +144,7 @@ public function testAsParameterEmptyArray(): void public function testStringable(): void { - $result = ParameterHelper::asParameter(new class() implements Stringable { + $result = ParameterHelper::asParameter(new class implements Stringable { public function __toString(): string { return 'abc'; From be6a56c17af30632c25b538b12c7c3a70cfcb448 Mon Sep 17 00:00:00 2001 From: Ghlen Nagels Date: Thu, 13 Feb 2025 15:07:06 +0530 Subject: [PATCH 10/10] fixed all typing errors and simplified maps and lists --- .github/workflows/static-analysis.yml | 13 +- .github/workflows/unit-test.yml | 2 +- composer.json | 4 +- psalm.xml | 12 - src/Bolt/BoltResult.php | 11 +- src/Bolt/BoltUnmanagedTransaction.php | 4 + src/Bolt/Session.php | 5 + src/Common/DriverSetupManager.php | 5 +- src/Common/TransactionHelper.php | 4 +- src/Contracts/CypherSequence.php | 204 ++++++ src/Databags/SummarizedResult.php | 30 +- src/Formatter/SummarizedResultFormatter.php | 19 +- src/ParameterHelper.php | 5 +- src/TypeCaster.php | 1 + src/Types/ArrayList.php | 255 -------- src/Types/CypherList.php | 312 ++++++++- src/Types/CypherMap.php | 600 +++++++++++++++--- ...erSequence.php => CypherSequenceTrait.php} | 222 ++----- src/Types/Map.php | 503 --------------- tests/Integration/EdgeCasesTest.php | 4 +- tests/Integration/Neo4jLoggerTest.php | 9 +- tests/Performance/PerformanceTest.php | 5 + tests/Unit/BoltConnectionPoolTest.php | 5 + tests/Unit/CypherListTest.php | 3 +- tests/Unit/CypherMapTest.php | 4 +- tests/Unit/ParameterHelperTest.php | 1 + 26 files changed, 1170 insertions(+), 1072 deletions(-) create mode 100644 src/Contracts/CypherSequence.php delete mode 100644 src/Types/ArrayList.php rename src/Types/{AbstractCypherSequence.php => CypherSequenceTrait.php} (65%) delete mode 100644 src/Types/Map.php diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index be9a04fc..7c6281de 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -7,24 +7,33 @@ on: pull_request: branches: - main - jobs: php-cs-fixer: name: "Lint & Analyse" runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + + # Setup the correct PHP version globally + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3.16' + - name: Cache Composer dependencies uses: actions/cache@v2 with: path: /tmp/composer-cache key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }} + - uses: php-actions/composer@v6 with: progress: yes - php_version: 8.3 + php_version: 8.3.16 version: 2 + - name: "PHP-CS-Fixer" run: vendor/bin/php-cs-fixer fix --dry-run + - name: "PSalm" run: vendor/bin/psalm --show-info=true diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 5dfe5b82..107bb284 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -14,7 +14,7 @@ jobs: name: "Running Unit Tests" strategy: matrix: - php: ['8.1', '8.2', '8.3'] + php: ['8.1.31', '8.2.27', '8.3.16'] steps: - uses: actions/checkout@v2 diff --git a/composer.json b/composer.json index be6b2da8..1c5a9221 100644 --- a/composer.json +++ b/composer.json @@ -48,9 +48,9 @@ "nyholm/psr7": "^1.3", "nyholm/psr7-server": "^1.0", "kriswallsmith/buzz": "^1.2", - "vimeo/psalm": "^5.0", + "vimeo/psalm": "^6.5", "friendsofphp/php-cs-fixer": "3.68.5", - "psalm/plugin-phpunit": "^0.18", + "psalm/plugin-phpunit": "^0.19", "monolog/monolog": "^2.2", "symfony/uid": "^5.0", "symfony/var-dumper": "^5.0", diff --git a/psalm.xml b/psalm.xml index fb61f69c..def87552 100755 --- a/psalm.xml +++ b/psalm.xml @@ -30,18 +30,6 @@ - - - - - - - - - - - - diff --git a/src/Bolt/BoltResult.php b/src/Bolt/BoltResult.php index f76676db..7d9c7408 100644 --- a/src/Bolt/BoltResult.php +++ b/src/Bolt/BoltResult.php @@ -26,7 +26,7 @@ /** * @psalm-import-type BoltCypherStats from SummarizedResultFormatter * - * @implements Iterator + * @implements Iterator> */ final class BoltResult implements Iterator { @@ -71,7 +71,7 @@ public function getIt(): Generator } /** - * @return Generator + * @return Generator> */ public function iterator(): Generator { @@ -113,10 +113,15 @@ private function fetchResults(): void } /** - * @return list + * @psalm-suppress InvalidNullableReturnType + * + * @return list */ public function current(): array { + /** + * @psalm-suppress NullableReturnStatement + */ return $this->getIt()->current(); } diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index 908552fd..bd318169 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -55,6 +55,8 @@ public function __construct( } /** + * @param iterable $statements + * * @throws ClientException|Throwable * * @return CypherList @@ -155,6 +157,8 @@ public function runStatement(Statement $statement): SummarizedResult } /** + * @param iterable $statements + * * @throws Throwable * * @return CypherList diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php index 5f4818a0..9365aee3 100644 --- a/src/Bolt/Session.php +++ b/src/Bolt/Session.php @@ -60,6 +60,8 @@ public function __construct( } /** + * @param iterable $statements + * * @return CypherList */ public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList @@ -120,6 +122,9 @@ public function transaction(callable $tsxHandler, ?TransactionConfiguration $con return $this->writeTransaction($tsxHandler, $config); } + /** + * @param iterable $statements + */ public function beginTransaction(?iterable $statements = null, ?TransactionConfiguration $config = null): UnmanagedTransactionInterface { $this->getLogger()?->log(LogLevel::INFO, 'Beginning transaction', ['statements' => $statements, 'config' => $config]); diff --git a/src/Common/DriverSetupManager.php b/src/Common/DriverSetupManager.php index fdc7c66f..f5d674bf 100644 --- a/src/Common/DriverSetupManager.php +++ b/src/Common/DriverSetupManager.php @@ -81,8 +81,9 @@ public function withSetup(DriverSetup $setup, ?string $alias = null, ?int $prior $setups = $this->driverSetups; - /** @var SplPriorityQueue */ - $setups[$alias] ??= new SplPriorityQueue(); + /** @var SplPriorityQueue $splPriorityQueue */ + $splPriorityQueue = new SplPriorityQueue(); + $setups[$alias] ??= $splPriorityQueue; /** @psalm-suppress ImpureMethodCall */ $setups[$alias]->insert($setup, $priority ?? 0); diff --git a/src/Common/TransactionHelper.php b/src/Common/TransactionHelper.php index f756c5fb..83c7e086 100644 --- a/src/Common/TransactionHelper.php +++ b/src/Common/TransactionHelper.php @@ -13,10 +13,10 @@ namespace Laudis\Neo4j\Common; +use Laudis\Neo4j\Contracts\CypherSequence; use Laudis\Neo4j\Contracts\TransactionInterface; use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; use Laudis\Neo4j\Exception\Neo4jException; -use Laudis\Neo4j\Types\AbstractCypherSequence; final class TransactionHelper { @@ -55,7 +55,7 @@ public static function retry(callable $tsxFactory, callable $tsxHandler) private static function triggerLazyResult(mixed $tbr): void { - if ($tbr instanceof AbstractCypherSequence) { + if ($tbr instanceof CypherSequence) { $tbr->preload(); } } diff --git a/src/Contracts/CypherSequence.php b/src/Contracts/CypherSequence.php new file mode 100644 index 00000000..a5de4e24 --- /dev/null +++ b/src/Contracts/CypherSequence.php @@ -0,0 +1,204 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Laudis\Neo4j\Contracts; + +use Countable; +use Iterator; +use JsonSerializable; +use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; +use Laudis\Neo4j\Types\Map; + +/** + * Abstract immutable sequence with basic functional methods. + * + * @template TValue + */ +interface CypherSequence extends Countable, JsonSerializable +{ + /** + * Copies the sequence. + * + * @return self + * + * @psalm-mutation-free + */ + public function copy(): self; + + /** + * Returns whether the sequence is empty. + * + * @psalm-suppress UnusedForeachValue + */ + public function isEmpty(): bool; + + /** + * 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 self + * + * @psalm-mutation-free + */ + public function merge(iterable $values): self; + + /** + * Checks if the sequence contains the given key. + */ + public function hasKey(string|int $key): bool; + + /** + * Checks if the sequence contains the given value. The equality check is strict. + */ + public function hasValue(mixed $value): bool; + + /** + * Creates a filtered the sequence with the provided callback. + * + * @param callable(TValue, array-key):bool $callback + * + * @return self + * + * @psalm-mutation-free + */ + public function filter(callable $callback): self; + + /** + * Maps the values of this sequence to a new one with the provided callback. + * + * @template ReturnType + * + * @param callable(TValue, array-key):ReturnType $callback + * + * @return self + * + * @psalm-mutation-free + */ + public function map(callable $callback): self; + + /** + * Reduces this sequence with the given callback. + * + * @template TInitial + * + * @param TInitial|null $initial + * @param callable(TInitial|null, TValue, array-key):TInitial $callback + * + * @return TInitial + */ + public function reduce(callable $callback, mixed $initial = null): mixed; + + /** + * Finds the position of the value within the sequence. + * + * @return false|array-key returns the key of the value if it is found, false otherwise + */ + public function find(mixed $value): false|string|int; + + /** + * Creates a reversed sequence. + * + * @return self + * + * @psalm-mutation-free + */ + public function reversed(): self; + + /** + * 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 self + * + * @psalm-mutation-free + */ + public function slice(int $offset, ?int $length = null): self; + + /** + * Creates a sorted sequence. If the comparator is null it will use natural ordering. + * + * @param (callable(TValue, TValue):int)|null $comparator + * + * @return self + * + * @psalm-mutation-free + */ + public function sorted(?callable $comparator = null): self; + + /** + * Creates a list from the arrays and objects in the sequence whose values corresponding with the provided key. + * + * @return CypherList + * + * @psalm-mutation-free + */ + public function pluck(string|int $key): CypherList; + + /** + * Uses the values found at the provided key as the key for the new Map. + * + * @return CypherMap + * + * @psalm-mutation-free + */ + public function keyBy(string|int $key): CypherMap; + + /** + * 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; + + /** + * Iterates over the sequence and applies the callable. + * + * @param callable(TValue, array-key):void $callable + * + * @return self + */ + public function each(callable $callable): self; + + /** + * Returns the sequence as an array. + * + * @return array + */ + public function toArray(): array; + + /** + * Returns the sequence as an array. + * + * @return array + */ + public function toRecursiveArray(): array; + + /** + * @return Iterator + */ + public function getGenerator(): Iterator; + + /** + * @return self + */ + public function withCacheLimit(int $cacheLimit): self; + + /** + * Preload the lazy evaluation. + */ + public function preload(): void; + + public function __serialize(): array; +} diff --git a/src/Databags/SummarizedResult.php b/src/Databags/SummarizedResult.php index 846966eb..15a4dd22 100644 --- a/src/Databags/SummarizedResult.php +++ b/src/Databags/SummarizedResult.php @@ -14,20 +14,23 @@ namespace Laudis\Neo4j\Databags; use Generator; -use Laudis\Neo4j\Types\AbstractCypherSequence; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Types\CypherList; +use Laudis\Neo4j\Types\CypherMap; /** * A result containing the values and the summary. * - * @extends CypherList + * @psalm-import-type OGMTypes from SummarizedResultFormatter + * + * @extends CypherList> */ final class SummarizedResult extends CypherList { private ?ResultSummary $summary = null; /** - * @param iterable|callable():Generator $iterable + * @param iterable>|callable():Generator> $iterable * * @psalm-mutation-free */ @@ -37,24 +40,6 @@ public function __construct(?ResultSummary &$summary, iterable|callable $iterabl $this->summary = &$summary; } - /** - * @template Value - * - * @param callable():(Generator) $operation - * - * @return static - * - * @psalm-mutation-free - */ - protected function withOperation($operation): AbstractCypherSequence - { - /** - * @psalm-suppress UnsafeInstantiation - * @psalm-suppress ImpurePropertyAssignment - */ - return new self($this->summary, $operation); - } - /** * Returns the result summary. */ @@ -68,6 +53,9 @@ public function getSummary(): ResultSummary return $this->summary; } + /** + * @return CypherList> + */ public function getResults(): CypherList { return new CypherList($this); diff --git a/src/Formatter/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php index 789c5438..6c345897 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -146,16 +146,19 @@ public function formatBoltStats(array $response): SummaryCounters ); } + /** + * @param BoltMeta $meta + */ 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( - /** @param {array{stats?: BoltCypherStats}&array} $response */ function (mixed $response) use ($connection, $statement, $runStart, $resultAvailableAfter, &$summary) { + /** @var array{stats?: BoltCypherStats}&array $response */ $stats = $this->formatBoltStats($response); $resultConsumedAfter = microtime(true) - $runStart; - $db = $response['db'] ?? ''; + $db = $response['stats']['db'] ?? ''; $summary = new ResultSummary( $stats, new DatabaseInfo($db), @@ -174,14 +177,12 @@ function (mixed $response) use ($connection, $statement, $runStart, $resultAvail ); }); - $formattedResult = $this->processBoltResult($meta, $result, $connection, $runStart, $resultAvailableAfter, $statement, $holder); + $formattedResult = $this->processBoltResult($meta, $result, $connection, $holder); /** - * @psalm-suppress MixedArgument - * * @var SummarizedResult> */ - return (new SummarizedResult($summary, $formattedResult))->withCacheLimit($result->getFetchSize()); + return new SummarizedResult($summary, (new CypherList($formattedResult))->withCacheLimit($result->getFetchSize())); } /** @@ -189,7 +190,7 @@ function (mixed $response) use ($connection, $statement, $runStart, $resultAvail * * @return CypherList> */ - private function processBoltResult(array $meta, BoltResult $result, BoltConnection $connection, float $runStart, float $resultAvailableAfter, Statement $statement, BookmarkHolder $holder): CypherList + private function processBoltResult(array $meta, BoltResult $result, BoltConnection $connection, BookmarkHolder $holder): CypherList { $tbr = (new CypherList(function () use ($result, $meta) { foreach ($result as $row) { @@ -209,6 +210,10 @@ private function processBoltResult(array $meta, BoltResult $result, BoltConnecti /** * @psalm-mutation-free + * + * @param BoltMeta $meta + * + * @return CypherMap */ private function formatRow(array $meta, array $result): CypherMap { diff --git a/src/ParameterHelper.php b/src/ParameterHelper.php index 83570a7f..520e23c8 100644 --- a/src/ParameterHelper.php +++ b/src/ParameterHelper.php @@ -20,7 +20,6 @@ use function count; use DateInterval; -use DateTime; use DateTimeInterface; use function get_debug_type; @@ -78,8 +77,6 @@ public static function asMap(iterable $iterable): CypherMap } /** - * @param DateTime|array|object|stdClass $value - * * @return iterable|scalar|stdClass|IStructure|null */ public static function asParameter( @@ -162,7 +159,7 @@ private static function filledIterableToArray(mixed $value, ConnectionProtocol $ } /** - * @param iterable $parameters + * @param iterable $parameters * * @return CypherMap */ diff --git a/src/TypeCaster.php b/src/TypeCaster.php index 053ca90c..1168091f 100644 --- a/src/TypeCaster.php +++ b/src/TypeCaster.php @@ -117,6 +117,7 @@ public static function toArray(mixed $value): ?array $tbr = []; /** @var mixed $x */ foreach ($value as $x) { + /** @psalm-suppress MixedAssignment */ $tbr[] = $x; } diff --git a/src/Types/ArrayList.php b/src/Types/ArrayList.php deleted file mode 100644 index 30e6747f..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/CypherList.php b/src/Types/CypherList.php index 29864c7f..4ea30b35 100644 --- a/src/Types/CypherList.php +++ b/src/Types/CypherList.php @@ -13,18 +13,238 @@ namespace Laudis\Neo4j\Types; +use AppendIterator; +use ArrayAccess; +use ArrayIterator; +use Generator; + +use function is_array; +use function is_callable; + +use Iterator; +use Laudis\Neo4j\Contracts\CypherSequence; use Laudis\Neo4j\Exception\RuntimeTypeException; use Laudis\Neo4j\TypeCaster; +use OutOfBoundsException; /** * An immutable ordered sequence of items. * * @template TValue * - * @extends ArrayList + * @implements CypherSequence + * @implements Iterator + * @implements ArrayAccess */ -class CypherList extends ArrayList +class CypherList implements CypherSequence, Iterator, ArrayAccess { + /** + * @use CypherSequenceTrait + */ + use CypherSequenceTrait; + + /** + * @param iterable|callable():Generator $iterable + * + * @psalm-mutation-free + */ + public function __construct(iterable|callable $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 self + * + * @psalm-mutation-free + */ + protected function withOperation(callable $operation): self + { + return new self($operation); + } + + /** + * Returns the first element in the sequence. + * + * @return TValue + */ + public function first() + { + foreach ($this as $value) { + return $value; + } + + throw new OutOfBoundsException('Cannot grab first element of an empty list'); + } + + /** + * Returns the last element in the sequence. + * + * @return TValue + */ + public function last() + { + if ($this->isEmpty()) { + throw new OutOfBoundsException('Cannot grab last element of an empty list'); + } + + $array = $this->toArray(); + + return $array[count($array) - 1]; + } + + /** + * @template NewValue + * + * @param iterable $values + * + * @return self + * + * @psalm-mutation-free + */ + public function merge(iterable $values): self + { + 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 CypherMap + */ + public function getAsMap(int $key): CypherMap + { + return $this->getAsCypherMap($key); + } + + /** + * @return CypherList + */ + public function getAsList(int $key): CypherList + { + return $this->getAsCypherList($key); + } + + /** + * @template Value + * + * @param iterable $iterable + * + * @return self + * + * @pure + */ + public static function fromIterable(iterable $iterable): self + { + return new self($iterable); + } + /** * @return CypherMap */ @@ -117,4 +337,92 @@ public function getAsWGS843DPoint(int $key): WGS843DPoint { return $this->getAsObject($key, WGS843DPoint::class); } + + public function key(): int + { + /** @var int */ + return $this->cacheKey(); + } + + /** + * @return array + */ + public function toArray(): array + { + $this->preload(); + + /** @var array */ + return $this->cache; + } + + /** + * @param callable(TValue, int):bool $callback + * + * @return self + * + * @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; + } + } + }); + } + + /** + * @template ReturnType + * + * @param callable(TValue, int):ReturnType $callback + * + * @return self + * + * @psalm-suppress ImplementedReturnTypeMismatch + * + * @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); + } + }); + } + + /** + * @template TInitial + * + * @param TInitial|null $initial + * @param callable(TInitial|null, TValue, int):TInitial $callback + * + * @return TInitial + */ + final public function reduce(callable $callback, mixed $initial = null): mixed + { + foreach ($this as $key => $value) { + $initial = $callback($initial, $value, $key); + } + + return $initial; + } + + /** + * Iterates over the sequence and applies the callable. + * + * @param callable(TValue, int):void $callable + * + * @return self + */ + public function each(callable $callable): self + { + foreach ($this as $key => $value) { + $callable($value, $key); + } + + return $this; + } } diff --git a/src/Types/CypherMap.php b/src/Types/CypherMap.php index 22426e80..270354df 100644 --- a/src/Types/CypherMap.php +++ b/src/Types/CypherMap.php @@ -13,200 +13,658 @@ namespace Laudis\Neo4j\Types; +use ArrayAccess; +use ArrayIterator; + use function func_num_args; +use Generator; +use Iterator; +use Laudis\Neo4j\Contracts\CypherSequence; +use Laudis\Neo4j\Databags\Pair; use Laudis\Neo4j\Exception\RuntimeTypeException; use Laudis\Neo4j\TypeCaster; +use OutOfBoundsException; +use stdClass; /** * An immutable ordered map of items. * * @template TValue * - * @extends Map + * @implements CypherSequence + * @implements ArrayAccess + * @implements Iterator */ -final class CypherMap extends Map +final class CypherMap implements CypherSequence, ArrayAccess, Iterator { /** - * @return CypherMap + * @use CypherSequenceTrait + */ + use CypherSequenceTrait { + jsonSerialize as jsonSerializeTrait; + } + + /** + * @param iterable|callable():Generator $iterable + * + * @psalm-mutation-free */ - public function getAsCypherMap(string $key, mixed $default = null): CypherMap + public function __construct(iterable|callable $iterable = []) { - if (func_num_args() === 1) { - $value = $this->get($key); + 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 { - $value = $this->get($key, $default); + $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; + } + }; } - $tbr = TypeCaster::toCypherMap($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, self::class); + } + + /** + * @template Value + * + * @param callable():(Generator) $operation + * + * @return self + * + * @psalm-mutation-free + */ + protected function withOperation(callable $operation): CypherMap + { + return new self($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'); + } - return $tbr; + /** + * 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]); } /** - * @return CypherList + * 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 CypherList + * + * @psalm-suppress UnusedForeachValue + */ + public function keys(): CypherList + { + return CypherList::fromIterable((function () { + foreach ($this as $key => $value) { + yield $key; + } + })()); + } + + /** + * Returns the pairs in the map in order. + * + * @return CypherList> + */ + public function pairs(): CypherList + { + return CypherList::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 self */ - public function getAsCypherList(string $key, mixed $default = null): CypherList + public function ksorted(?callable $comparator = null): CypherMap + { + 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 CypherList + */ + public function values(): CypherList + { + return CypherList::fromIterable((function () { + yield from $this; + })()); + } + + /** + * Creates a new map using exclusive or on the keys. + * + * @param iterable $map + * + * @return self + */ + public function xor(iterable $map): CypherMap + { + return $this->withOperation(function () use ($map) { + $map = CypherMap::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): CypherMap + { + return $this->withOperation(function () use ($values) { + $tbr = $this->toArray(); + $values = CypherMap::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 self + */ + public function union(iterable $map): CypherMap + { + return $this->withOperation(function () use ($map) { + $map = CypherMap::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 self + */ + public function intersect(iterable $map): CypherMap + { + return $this->withOperation(function () use ($map) { + $map = CypherMap::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 self + */ + public function diff(iterable $map): CypherMap + { + return $this->withOperation(function () use ($map) { + $map = CypherMap::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 $this->jsonSerializeTrait(); + } + + public function getAsString(string $key): string { if (func_num_args() === 1) { $value = $this->get($key); } else { - $value = $this->get($key, $default); + $value = $this->get($key); } - $tbr = TypeCaster::toCypherList($value); + $tbr = TypeCaster::toString($value); if ($tbr === null) { - throw new RuntimeTypeException($value, CypherList::class); + throw new RuntimeTypeException($value, 'string'); } return $tbr; } - public function getAsDate(string $key, mixed $default = null): Date + public function getAsInt(string $key): int { if (func_num_args() === 1) { - return $this->getAsObject($key, Date::class); + $value = $this->get($key); + } else { + $value = $this->get($key); + } + $tbr = TypeCaster::toInt($value); + if ($tbr === null) { + throw new RuntimeTypeException($value, 'int'); } - return $this->getAsObject($key, Date::class, $default); + return $tbr; } - public function getAsDateTime(string $key, mixed $default = null): DateTime + public function getAsFloat(string $key): float { if (func_num_args() === 1) { - return $this->getAsObject($key, DateTime::class); + $value = $this->get($key); + } else { + $value = $this->get($key); + } + $tbr = TypeCaster::toFloat($value); + if ($tbr === null) { + throw new RuntimeTypeException($value, 'float'); } - return $this->getAsObject($key, DateTime::class, $default); + return $tbr; } - public function getAsDuration(string $key, mixed $default = null): Duration + public function getAsBool(string $key): bool { if (func_num_args() === 1) { - return $this->getAsObject($key, Duration::class); + $value = $this->get($key); + } else { + $value = $this->get($key); + } + $tbr = TypeCaster::toBool($value); + if ($tbr === null) { + throw new RuntimeTypeException($value, 'bool'); } - return $this->getAsObject($key, Duration::class, $default); + return $tbr; } - public function getAsLocalDateTime(string $key, mixed $default = null): LocalDateTime + /** + * @return null + */ + public function getAsNull(string $key) { if (func_num_args() === 1) { - return $this->getAsObject($key, LocalDateTime::class); + /** @psalm-suppress UnusedMethodCall */ + $this->get($key); } - return $this->getAsObject($key, LocalDateTime::class, $default); + return TypeCaster::toNull(); } - public function getAsLocalTime(string $key, mixed $default = null): LocalTime + /** + * @template U + * + * @param class-string $class + * + * @return U + */ + public function getAsObject(string $key, string $class): object { - if (func_num_args() === 1) { - return $this->getAsObject($key, LocalTime::class); + $value = $this->get($key); + $tbr = TypeCaster::toClass($value, $class); + if ($tbr === null) { + throw new RuntimeTypeException($value, $class); } - return $this->getAsObject($key, LocalTime::class, $default); + return $tbr; } - public function getAsTime(string $key, mixed $default = null): Time + /** + * @template Value + * + * @param iterable $iterable + * + * @return CypherMap + */ + public static function fromIterable(iterable $iterable): CypherMap { - if (func_num_args() === 1) { - return $this->getAsObject($key, Time::class); + return new self($iterable); + } + + /** + * @return CypherMap + */ + public function getAsCypherMap(string $key): CypherMap + { + $value = $this->get($key); + $tbr = TypeCaster::toCypherMap($value); + if ($tbr === null) { + throw new RuntimeTypeException($value, self::class); } - return $this->getAsObject($key, Time::class, $default); + return $tbr; } - public function getAsNode(string $key, mixed $default = null): Node + /** + * @return CypherList + */ + public function getAsCypherList(string $key): CypherList { - if (func_num_args() === 1) { - return $this->getAsObject($key, Node::class); + $value = $this->get($key); + $tbr = TypeCaster::toCypherList($value); + if ($tbr === null) { + throw new RuntimeTypeException($value, CypherList::class); } - return $this->getAsObject($key, Node::class, $default); + return $tbr; } - public function getAsRelationship(string $key, mixed $default = null): Relationship + public function getAsDate(string $key): Date { - if (func_num_args() === 1) { - return $this->getAsObject($key, Relationship::class); - } + return $this->getAsObject($key, Date::class); + } - return $this->getAsObject($key, Relationship::class, $default); + public function getAsDateTime(string $key): DateTime + { + return $this->getAsObject($key, DateTime::class); } - public function getAsPath(string $key, mixed $default = null): Path + public function getAsDuration(string $key): Duration { - if (func_num_args() === 1) { - return $this->getAsObject($key, Path::class); - } + return $this->getAsObject($key, Duration::class); + } + + public function getAsLocalDateTime(string $key): LocalDateTime + { + return $this->getAsObject($key, LocalDateTime::class); + } - return $this->getAsObject($key, Path::class, $default); + public function getAsLocalTime(string $key): LocalTime + { + return $this->getAsObject($key, LocalTime::class); } - public function getAsCartesian3DPoint(string $key, mixed $default = null): Cartesian3DPoint + public function getAsTime(string $key): Time { if (func_num_args() === 1) { - return $this->getAsObject($key, Cartesian3DPoint::class); + return $this->getAsObject($key, Time::class); } - return $this->getAsObject($key, Cartesian3DPoint::class, $default); + return $this->getAsObject($key, Time::class); + } + + public function getAsNode(string $key): Node + { + return $this->getAsObject($key, Node::class); + } + + public function getAsRelationship(string $key): Relationship + { + return $this->getAsObject($key, Relationship::class); } - public function getAsCartesianPoint(string $key, mixed $default = null): CartesianPoint + public function getAsPath(string $key): Path + { + return $this->getAsObject($key, Path::class); + } + + public function getAsCartesian3DPoint(string $key): Cartesian3DPoint + { + return $this->getAsObject($key, Cartesian3DPoint::class); + } + + public function getAsCartesianPoint(string $key): CartesianPoint { if (func_num_args() === 1) { return $this->getAsObject($key, CartesianPoint::class); } - return $this->getAsObject($key, CartesianPoint::class, $default); + return $this->getAsObject($key, CartesianPoint::class); } - public function getAsWGS84Point(string $key, mixed $default = null): WGS84Point + public function getAsWGS84Point(string $key): WGS84Point { if (func_num_args() === 1) { return $this->getAsObject($key, WGS84Point::class); } - return $this->getAsObject($key, WGS84Point::class, $default); + return $this->getAsObject($key, WGS84Point::class); } - public function getAsWGS843DPoint(string $key, mixed $default = null): WGS843DPoint + public function getAsWGS843DPoint(string $key): WGS843DPoint { if (func_num_args() === 1) { return $this->getAsObject($key, WGS843DPoint::class); } - return $this->getAsObject($key, WGS843DPoint::class, $default); + return $this->getAsObject($key, WGS843DPoint::class); + } + + public function key(): string + { + /** @var string */ + return $this->cacheKey(); } /** - * @template Value - * - * @param iterable $iterable + * @return array + */ + public function toArray(): array + { + $this->preload(); + + /** @var array */ + return $this->cache; + } + + public function getAsMap(string $string): CypherMap + { + return $this->getAsCypherMap($string); + } + + public function getAsArrayList(string $string): CypherList + { + return $this->getAsCypherList($string); + } + + /** + * @param callable(TValue, string):bool $callback * - * @return self + * @return self * - * @pure + * @psalm-mutation-free */ - public static function fromIterable(iterable $iterable): CypherMap + final public function filter(callable $callback): self { - return new self($iterable); + return $this->withOperation(function () use ($callback) { + foreach ($this as $key => $value) { + if ($callback($value, $key)) { + yield $key => $value; + } + } + }); } /** + * @template ReturnType + * + * @param callable(TValue, string):ReturnType $callback + * + * @return self + * + * @psalm-suppress ImplementedReturnTypeMismatch + * * @psalm-mutation-free */ - public function pluck(string $key): CypherList + final public function map(callable $callback): self { - return CypherList::fromIterable(parent::pluck($key)); + return $this->withOperation(function () use ($callback) { + foreach ($this as $key => $value) { + yield $key => $callback($value, $key); + } + }); } /** - * @psalm-mutation-free + * @template TInitial + * + * @param TInitial|null $initial + * @param callable(TInitial|null, TValue, string):TInitial $callback + * + * @return TInitial */ - public function keyBy(string $key): CypherMap + final public function reduce(callable $callback, mixed $initial = null): mixed { - return CypherMap::fromIterable(parent::keyBy($key)); + foreach ($this as $key => $value) { + $initial = $callback($initial, $value, $key); + } + + return $initial; + } + + /** + * Iterates over the sequence and applies the callable. + * + * @param callable(TValue, string):void $callable + * + * @return self + */ + public function each(callable $callable): self + { + foreach ($this as $key => $value) { + $callable($value, $key); + } + + return $this; } } diff --git a/src/Types/AbstractCypherSequence.php b/src/Types/CypherSequenceTrait.php similarity index 65% rename from src/Types/AbstractCypherSequence.php rename to src/Types/CypherSequenceTrait.php index 8b82e344..9a5f9311 100644 --- a/src/Types/AbstractCypherSequence.php +++ b/src/Types/CypherSequenceTrait.php @@ -23,7 +23,6 @@ use function call_user_func; use function count; -use Countable; use Generator; use function get_object_vars; @@ -38,7 +37,7 @@ use function is_string; use Iterator; -use JsonSerializable; +use Laudis\Neo4j\Contracts\CypherSequence; use function method_exists; @@ -55,23 +54,19 @@ * 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 +trait CypherSequenceTrait { - /** @var list */ + /** @var list */ protected array $keyCache = []; - /** @var array */ + /** @var array */ protected array $cache = []; private int $cacheLimit = PHP_INT_MAX; protected int $currentPosition = 0; protected int $generatorPosition = 0; /** - * @var (callable():(Iterator))|Iterator + * @var (callable():(Iterator))|Iterator */ protected $generator; @@ -80,16 +75,14 @@ abstract class AbstractCypherSequence implements Countable, JsonSerializable, Ar * * @param callable():(Generator) $operation * - * @return static + * @return self * * @psalm-mutation-free */ abstract protected function withOperation(callable $operation): self; /** - * Copies the sequence. - * - * @return static + * @return self * * @psalm-mutation-free */ @@ -100,15 +93,8 @@ final public function copy(): self }); } - /** - * mixed - * Returns whether the sequence is empty. - * - * @psalm-suppress UnusedForeachValue - */ final public function isEmpty(): bool { - /** @noinspection PhpLoopNeverIteratesInspection */ foreach ($this as $ignored) { return false; } @@ -116,106 +102,17 @@ final public function isEmpty(): bool 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 + final public function hasKey(string|int $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 + final public function hasValue(mixed $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) + final public function find(mixed $value): false|string|int { foreach ($this as $i => $x) { if ($value === $x) { @@ -229,7 +126,7 @@ final public function find($value) /** * Creates a reversed sequence. * - * @return static + * @return self * * @psalm-mutation-free */ @@ -244,7 +141,7 @@ public function reversed(): self * 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 + * @return self * * @psalm-mutation-free */ @@ -274,7 +171,7 @@ public function slice(int $offset, ?int $length = null): self * * @param (callable(TValue, TValue):int)|null $comparator * - * @return static + * @return self * * @psalm-mutation-free */ @@ -296,19 +193,18 @@ public function sorted(?callable $comparator = null): self /** * Creates a list from the arrays and objects in the sequence whose values corresponding with the provided key. * - * @return ArrayList + * @return CypherList * * @psalm-mutation-free - * - * @psalm-suppress MixedArrayAccess */ - public function pluck(string $key): ArrayList + public function pluck(string|int $key): CypherList { - return new ArrayList(function () use ($key) { + return new CypherList(function () use ($key) { foreach ($this as $value) { if ((is_array($value) && array_key_exists($key, $value)) || ($value instanceof ArrayAccess && $value->offsetExists($key))) { + /** @psalm-suppress MixedArrayAccess false positive */ yield $value[$key]; - } elseif (is_object($value) && property_exists($value, $key)) { + } elseif (is_string($key) && is_object($value) && property_exists($value, $key)) { yield $value->$key; } } @@ -318,19 +214,19 @@ public function pluck(string $key): ArrayList /** * Uses the values found at the provided key as the key for the new Map. * - * @return Map + * @return CypherMap * * @psalm-mutation-free * * @psalm-suppress MixedArrayAccess */ - public function keyBy(string $key): Map + public function keyBy(string|int $key): CypherMap { - return new Map(function () use ($key) { + return new CypherMap(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)) { + } elseif (is_string($key) && 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'); @@ -349,21 +245,10 @@ public function join(?string $glue = null): string } /** - * Iterates over the sequence and applies the callable. - * - * @param callable(TValue, TKey):void $callable + * @param array-key $offset * - * @return static + * @return TValue */ - 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()) { @@ -371,24 +256,31 @@ public function offsetGet(mixed $offset): mixed } if (!array_key_exists($offset, $this->cache)) { - throw new OutOfBoundsException(sprintf('Offset: "%s" does not exists in object of instance: %s', $offset, static::class)); + throw new OutOfBoundsException(sprintf('Offset: "%s" does not exists in object of instance: %s', $offset, self::class)); } return $this->cache[$offset]; } + /** + * @param array-key $offset + * @param TValue $value + */ public function offsetSet(mixed $offset, mixed $value): void { - throw new BadMethodCallException(sprintf('%s is immutable', static::class)); + throw new BadMethodCallException(sprintf('%s is immutable', self::class)); } + /** + * @param array-key $offset + */ public function offsetUnset(mixed $offset): void { - throw new BadMethodCallException(sprintf('%s is immutable', static::class)); + throw new BadMethodCallException(sprintf('%s is immutable', self::class)); } /** - * @param TKey $offset + * @param array-key $offset * * @psalm-suppress UnusedForeachValue */ @@ -409,24 +301,12 @@ public function jsonSerialize(): mixed /** * 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 + * @return array */ final public function toRecursiveArray(): array { return $this->map(static function ($x) { - if ($x instanceof self) { + if ($x instanceof CypherSequence) { return $x->toRecursiveArray(); } @@ -472,9 +352,10 @@ public function next(): void $generator->next(); if ($generator->valid()) { - /** @var TKey */ - $this->keyCache[] = $generator->key(); - $this->cache[$generator->key()] = $generator->current(); + /** @var array-key */ + $key = $generator->key(); + $this->keyCache[] = $key; + $this->cache[$key] = $generator->current(); } ++$this->generatorPosition; ++$this->currentPosition; @@ -483,24 +364,13 @@ public function next(): void } } - /** - * @return TKey - */ - public function key(): mixed - { - return $this->cacheKey(); - } - - /** - * @return TKey - */ - protected function cacheKey() + protected function cacheKey(): string|int { return $this->keyCache[$this->currentPosition % max($this->cacheLimit, 1)]; } /** - * @return Iterator + * @return Iterator */ public function getGenerator(): Iterator { @@ -512,7 +382,7 @@ public function getGenerator(): Iterator } /** - * @return static + * @return self */ public function withCacheLimit(int $cacheLimit): self { @@ -534,7 +404,7 @@ private function setupCache(): void } if ($this->cache === [] && $generator->valid()) { - /** @var TKey $key */ + /** @var array-key $key */ $key = $generator->key(); $this->cache[$key] = $generator->current(); $this->keyCache[] = $key; @@ -553,8 +423,6 @@ public function preload(): void /** * @psalm-mutation-free - * - * @param {int|string|object} $key */ protected function isStringable(mixed $key): bool { diff --git a/src/Types/Map.php b/src/Types/Map.php deleted file mode 100644 index 712f0b9d..00000000 --- a/src/Types/Map.php +++ /dev/null @@ -1,503 +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 { - $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 { - $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 { - $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 { - $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 { - $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 { - $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 { - $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/tests/Integration/EdgeCasesTest.php b/tests/Integration/EdgeCasesTest.php index cc19152b..9157683e 100644 --- a/tests/Integration/EdgeCasesTest.php +++ b/tests/Integration/EdgeCasesTest.php @@ -17,7 +17,7 @@ use Laudis\Neo4j\Databags\Statement; use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest; use Laudis\Neo4j\Tests\Fixtures\MoviesFixture; -use Laudis\Neo4j\Types\ArrayList; +use Laudis\Neo4j\Types\CypherList; use Laudis\Neo4j\Types\CypherMap; use Laudis\Neo4j\Types\Node; use Laudis\Neo4j\Types\Path; @@ -63,7 +63,7 @@ public function testComplex(): void self::assertTrue($actorInfo->hasKey('roles')); self::assertTrue($actorInfo->hasKey('movie')); - self::assertInstanceOf(ArrayList::class, $actorInfo->get('roles')); + self::assertInstanceOf(CypherList::class, $actorInfo->get('roles')); self::assertInstanceOf(Node::class, $actorInfo->get('movie')); // this can be a cyphermap in HTTP protocol $point = $actorInfo->getAsNode('movie')->getProperty('point'); diff --git a/tests/Integration/Neo4jLoggerTest.php b/tests/Integration/Neo4jLoggerTest.php index 40f9bd06..acb618c4 100644 --- a/tests/Integration/Neo4jLoggerTest.php +++ b/tests/Integration/Neo4jLoggerTest.php @@ -135,9 +135,14 @@ static function (string $message, array $context) use (&$debugLogs) { self::assertCount(3, $infoLogs); self::assertEquals(array_slice($expectedInfoLogs, 0, 2), array_slice($infoLogs, 0, 2)); - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ + /** + * @psalm-suppress PossiblyUndefinedIntArrayOffset + */ self::assertEquals($expectedInfoLogs[2][0], $infoLogs[2][0]); - /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ + /** + * @psalm-suppress PossiblyUndefinedIntArrayOffset + * @psalm-suppress MixedArrayAccess + */ self::assertInstanceOf(SessionConfiguration::class, $infoLogs[2][1]['sessionConfig']); self::assertEquals($expectedDebugLogs, $debugLogs); diff --git a/tests/Performance/PerformanceTest.php b/tests/Performance/PerformanceTest.php index cb78912e..676dbcc7 100644 --- a/tests/Performance/PerformanceTest.php +++ b/tests/Performance/PerformanceTest.php @@ -31,6 +31,7 @@ use function sleep; use Symfony\Component\Uid\Uuid; +use UnexpectedValueException; final class PerformanceTest extends EnvironmentAwareIntegrationTest { @@ -136,6 +137,10 @@ private function addTransactionOrRun(int $i, array $tsxs, int $retriesLeft = 10) private function testAndDestructTransaction(array $tsxs, int $j, int $retriesLeft = 10): array { $tsx = array_pop($tsxs); + if ($tsx === null) { + throw new UnexpectedValueException(); + } + try { $x = $tsx->run('RETURN 1 AS x')->first()->get('x'); diff --git a/tests/Unit/BoltConnectionPoolTest.php b/tests/Unit/BoltConnectionPoolTest.php index 3c77634a..8ecc2b33 100644 --- a/tests/Unit/BoltConnectionPoolTest.php +++ b/tests/Unit/BoltConnectionPoolTest.php @@ -34,6 +34,8 @@ use function sleep; +use UnexpectedValueException; + class BoltConnectionPoolTest extends TestCase { private ConnectionPool $pool; @@ -131,6 +133,9 @@ private function refCount($var): int ob_start(); debug_zval_dump($var); $dump = ob_get_clean(); + if ($dump === false) { + throw new UnexpectedValueException(); + } $matches = []; preg_match('/refcount\(([0-9]+)/', $dump, $matches); diff --git a/tests/Unit/CypherListTest.php b/tests/Unit/CypherListTest.php index f83e155f..520ce2d4 100644 --- a/tests/Unit/CypherListTest.php +++ b/tests/Unit/CypherListTest.php @@ -160,7 +160,7 @@ public function testFilterBlock(): void public function testFilterSelective(): void { - $filter = $this->list->filter(static fn (string $x, int $i) => $x === 'B' || $i === 2)->toArray(); + $filter = $this->list->filter(static fn (string $x, $i) => $x === 'B' || $i === 2)->toArray(); self::assertEquals(['B', 'C'], $filter); } @@ -386,7 +386,6 @@ public function testSortedCustom(): void public function testEach(): void { $cntr = -1; - /** @psalm-suppress UnusedClosureParam */ $this->list->each(static function (string $x, int $key) use (&$cntr) { $cntr = $key; }); self::assertEquals($this->list->count() - 1, $cntr); diff --git a/tests/Unit/CypherMapTest.php b/tests/Unit/CypherMapTest.php index fc63025f..b0315836 100644 --- a/tests/Unit/CypherMapTest.php +++ b/tests/Unit/CypherMapTest.php @@ -24,7 +24,7 @@ use Laudis\Neo4j\Databags\Pair; use Laudis\Neo4j\Exception\RuntimeTypeException; -use Laudis\Neo4j\Types\ArrayList; +use Laudis\Neo4j\Types\CypherList; use Laudis\Neo4j\Types\CypherMap; use OutOfBoundsException; use PHPUnit\Framework\TestCase; @@ -405,7 +405,7 @@ public function testKeys(): void public function testPairs(): void { - $list = new ArrayList([new Pair('A', 'x'), new Pair('B', 'y'), new Pair('C', 'z')]); + $list = new CypherList([new Pair('A', 'x'), new Pair('B', 'y'), new Pair('C', 'z')]); self::assertEquals($list->toArray(), $this->map->pairs()->toArray()); } diff --git a/tests/Unit/ParameterHelperTest.php b/tests/Unit/ParameterHelperTest.php index b45e294b..2a18b8e1 100644 --- a/tests/Unit/ParameterHelperTest.php +++ b/tests/Unit/ParameterHelperTest.php @@ -110,6 +110,7 @@ public function testFormatParameterIterable(): void public function testFormatParameterInvalidIterable(): void { $this->expectException(InvalidArgumentException::class); + /** @psalm-suppress MixedArgumentTypeCoercion */ ParameterHelper::formatParameters(self::$invalidIterable, ConnectionProtocol::BOLT_V44()); }