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/.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/README.md b/README.md index 020a749c..4c9040f6 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 and 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/composer.json b/composer.json index ffbc9954..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", - "friendsofphp/php-cs-fixer": "3.15.0", - "psalm/plugin-phpunit": "^0.18", + "vimeo/psalm": "^6.5", + "friendsofphp/php-cs-fixer": "3.68.5", + "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/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 afb6db66..0264cebc 100644 --- a/src/Basic/Client.php +++ b/src/Basic/Client.php @@ -18,19 +18,13 @@ 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 - ) {} + 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 7b73a876..937949d9 100644 --- a/src/Basic/Driver.php +++ b/src/Basic/Driver.php @@ -17,25 +17,19 @@ use Laudis\Neo4j\Contracts\DriverInterface; use Laudis\Neo4j\Databags\DriverConfiguration; use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\DriverFactory; use Laudis\Neo4j\Formatter\SummarizedResultFormatter; -use Laudis\Neo4j\Types\CypherMap; use Psr\Http\Message\UriInterface; -/** - * @implements DriverInterface> - */ final class Driver implements DriverInterface { /** - * @param DriverInterface> $driver - * * @psalm-external-mutation-free */ public function __construct( - private readonly DriverInterface $driver - ) {} + private readonly DriverInterface $driver, + ) { + } /** * @psalm-mutation-free @@ -52,7 +46,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..e417055e 100644 --- a/src/Basic/Session.php +++ b/src/Basic/Session.php @@ -19,33 +19,24 @@ use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; -/** - * @implements SessionInterface> - */ final class Session implements SessionInterface { - /** - * @param SessionInterface> $session - */ public function __construct( - private readonly SessionInterface $session - ) {} + private readonly SessionInterface $session, + ) { + } /** * @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 233e582d..d30f820e 100644 --- a/src/Basic/UnmanagedTransaction.php +++ b/src/Basic/UnmanagedTransaction.php @@ -17,33 +17,22 @@ 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 - ) {} + private readonly UnmanagedTransactionInterface $tsx, + ) { + } /** * @param iterable $parameters - * - * @return SummarizedResult */ public function run(string $statement, iterable $parameters = []): SummarizedResult { return $this->tsx->run($statement, $parameters); } - /** - * @return SummarizedResult - */ public function runStatement(Statement $statement): SummarizedResult { return $this->tsx->runStatement($statement); @@ -52,7 +41,7 @@ public function runStatement(Statement $statement): SummarizedResult /** * @param iterable $statements * - * @return CypherList> + * @return CypherList */ public function runStatements(iterable $statements): CypherList { @@ -62,7 +51,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..07a41a48 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -27,22 +27,23 @@ 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; +use Throwable; use WeakReference; /** * @implements ConnectionInterface * - * @psalm-import-type BoltMeta from FormatterInterface + * @psalm-import-type BoltMeta from SummarizedResultFormatter */ class BoltConnection implements ConnectionInterface { @@ -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 7c21c484..c8eb3d91 100644 --- a/src/Bolt/BoltDriver.php +++ b/src/Bolt/BoltDriver.php @@ -23,50 +23,34 @@ 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 +64,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..7d9c7408 100644 --- a/src/Bolt/BoltResult.php +++ b/src/Bolt/BoltResult.php @@ -21,12 +21,12 @@ use function in_array; use Iterator; -use Laudis\Neo4j\Contracts\FormatterInterface; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; /** - * @psalm-import-type BoltCypherStats from FormatterInterface + * @psalm-import-type BoltCypherStats from SummarizedResultFormatter * - * @implements Iterator + * @implements Iterator> */ final class BoltResult implements Iterator { @@ -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 { @@ -70,7 +71,7 @@ public function getIt(): Generator } /** - * @return Generator + * @return Generator> */ public function iterator(): Generator { @@ -112,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 9a547551..bd318169 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -14,16 +14,16 @@ 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; 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; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\ParameterHelper; -use Laudis\Neo4j\Types\AbstractCypherSequence; use Laudis\Neo4j\Types\CypherList; use function microtime; @@ -33,35 +33,33 @@ /** * Manages a transaction over the bolt protocol. * - * @template T - * - * @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, private readonly TransactionConfiguration $tsxConfig, - private readonly BookmarkHolder $bookmarkHolder - ) {} + private readonly BookmarkHolder $bookmarkHolder, + ) { + } /** + * @param iterable $statements + * * @throws ClientException|Throwable + * + * @return CypherList */ public function commit(iterable $statements = []): CypherList { @@ -81,10 +79,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(); @@ -116,7 +112,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)); } @@ -124,7 +120,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); @@ -161,11 +157,14 @@ public function runStatement(Statement $statement) } /** + * @param iterable $statements + * * @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/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 8d895d59..9365aee3 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,11 +54,16 @@ public function __construct( /** * @psalm-readonly */ - private readonly FormatterInterface $formatter + private readonly SummarizedResultFormatter $formatter, ) { $this->bookmarkHolder = new BookmarkHolder(Bookmark::from($config->getBookmarks())); } + /** + * @param iterable $statements + * + * @return CypherList + */ public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList { $tbr = []; @@ -84,12 +85,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); } @@ -121,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]); @@ -133,11 +137,11 @@ public function beginTransaction(?iterable $statements = null, ?TransactionConfi } /** - * @return UnmanagedTransactionInterface + * @return UnmanagedTransactionInterface */ 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 9d862061..4a114f66 100644 --- a/src/Client.php +++ b/src/Client.php @@ -21,39 +21,35 @@ 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 */ public function __construct( private readonly DriverSetupManager $driverSetups, private readonly SessionConfiguration $defaultSessionConfiguration, - private readonly TransactionConfiguration $defaultTransactionConfiguration - ) {} + private readonly TransactionConfiguration $defaultTransactionConfiguration, + ) { + } public function getDriverSetups(): DriverSetupManager { @@ -70,12 +66,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(); } @@ -84,8 +80,9 @@ 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])]; } @@ -126,9 +123,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 +181,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..42a79232 100644 --- a/src/ClientBuilder.php +++ b/src/ClientBuilder.php @@ -20,24 +20,18 @@ 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; /** * Immutable factory for creating a client. * - * @template T - * - * @psalm-import-type OGMTypes from OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter */ final class ClientBuilder { @@ -45,8 +39,6 @@ final class ClientBuilder /** * @psalm-mutation-free - * - * @param DriverSetupManager $driverSetups */ public function __construct( /** @psalm-readonly */ @@ -54,12 +46,11 @@ public function __construct( /** @psalm-readonly */ private TransactionConfiguration $defaultTransactionConfig, private DriverSetupManager $driverSetups, - ) {} + ) { + } /** * 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 +68,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 +80,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 +98,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 +109,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 +121,6 @@ public function withFormatter(FormatterInterface $formatter): self } /** - * @return ClientInterface - * * @psalm-mutation-free */ public function build(): ClientInterface @@ -156,8 +133,6 @@ public function build(): ClientInterface } /** - * @return self - * * @psalm-mutation-free */ public function withDefaultDriverConfiguration(DriverConfiguration $config): self @@ -170,8 +145,6 @@ public function withDefaultDriverConfiguration(DriverConfiguration $config): sel } /** - * @return self - * * @psalm-mutation-free */ public function withDefaultSessionConfiguration(SessionConfiguration $config): self @@ -183,8 +156,6 @@ public function withDefaultSessionConfiguration(SessionConfiguration $config): s } /** - * @return self - * * @psalm-mutation-free */ public function withDefaultTransactionConfiguration(TransactionConfiguration $config): self 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 bb2639d2..f5d674bf 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; @@ -36,28 +36,24 @@ 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; /** * @psalm-mutation-free - * - * @param FormatterInterface $formatter */ public function __construct( - private FormatterInterface $formatter, - private DriverConfiguration $configuration - ) {} + private SummarizedResultFormatter $formatter, + private DriverConfiguration $configuration, + ) { + } public function getDriverConfiguration(): DriverConfiguration { @@ -85,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); @@ -101,9 +98,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); @@ -193,7 +187,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/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/TransactionHelper.php b/src/Common/TransactionHelper.php index a9be078e..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 { @@ -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 */ @@ -56,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/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/Contracts/ClientInterface.php b/src/Contracts/ClientInterface.php index 5a5f1d4c..389c35b7 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/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/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/FormatterInterface.php b/src/Contracts/FormatterInterface.php deleted file mode 100644 index d2198dfe..00000000 --- a/src/Contracts/FormatterInterface.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Contracts; - -use Bolt\Bolt; -use Laudis\Neo4j\Bolt\BoltConnection; -use Laudis\Neo4j\Bolt\BoltResult; -use Laudis\Neo4j\Databags\BookmarkHolder; -use Laudis\Neo4j\Databags\Statement; - -/** - * 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); -} diff --git a/src/Contracts/SessionInterface.php b/src/Contracts/SessionInterface.php index 57eeeb5b..1694c9a5 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..ff615d8f 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..f04697a0 100644 --- a/src/Contracts/UnmanagedTransactionInterface.php +++ b/src/Contracts/UnmanagedTransactionInterface.php @@ -14,15 +14,12 @@ namespace Laudis\Neo4j\Contracts; use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Types\CypherList; /** * An unmanaged transaction needs to be committed or rolled back manually. * - * @template T - * - * @extends TransactionInterface - * * @see https://neo4j.com/docs/cypher-manual/current/introduction/transactions/ */ interface UnmanagedTransactionInterface extends TransactionInterface @@ -32,7 +29,7 @@ interface UnmanagedTransactionInterface extends TransactionInterface * * @param iterable $statements * - * @return CypherList + * @return CypherList */ public function commit(iterable $statements = []): CypherList; 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/SummarizedResult.php b/src/Databags/SummarizedResult.php index e72249a2..15a4dd22 100644 --- a/src/Databags/SummarizedResult.php +++ b/src/Databags/SummarizedResult.php @@ -14,49 +14,32 @@ 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. * - * @template TValue + * @psalm-import-type OGMTypes from SummarizedResultFormatter * - * @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; } - /** - * @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. */ @@ -70,6 +53,9 @@ public function getSummary(): ResultSummary return $this->summary; } + /** + * @return CypherList> + */ public function getResults(): CypherList { return new CypherList($this); 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/DriverFactory.php b/src/DriverFactory.php index aa48c00e..32a16cdd 100644 --- a/src/DriverFactory.php +++ b/src/DriverFactory.php @@ -20,34 +20,23 @@ 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\OGMFormatter; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Neo4j\Neo4jDriver; use Psr\Http\Message\UriInterface; /** * Factory for creating drivers directly. * - * @psalm-import-type OGMResults from OGMFormatter + * @psalm-import-type OGMResults from SummarizedResultFormatter */ 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 6de6b90e..00000000 --- a/src/Formatter/OGMFormatter.php +++ /dev/null @@ -1,116 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Formatter; - -use function array_key_exists; - -use Laudis\Neo4j\Bolt\BoltConnection; -use Laudis\Neo4j\Bolt\BoltResult; -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\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; - -/** - * 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, - ) {} - - /** - * Creates a new instance of itself. - * - * @pure - */ - public static function create(): OGMFormatter - { - return new self(new BoltOGMTranslator()); - } - - /** - * @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; - } - - /** - * @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); - } -} diff --git a/src/Formatter/Specialised/BoltOGMTranslator.php b/src/Formatter/Specialised/BoltOGMTranslator.php index bdeb5ecd..16165eab 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 * @@ -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 27b29842..6c345897 100644 --- a/src/Formatter/SummarizedResultFormatter.php +++ b/src/Formatter/SummarizedResultFormatter.php @@ -18,7 +18,7 @@ use Laudis\Neo4j\Bolt\BoltConnection; use Laudis\Neo4j\Bolt\BoltResult; -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; @@ -27,38 +27,87 @@ use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\SummaryCounters; use Laudis\Neo4j\Enum\QueryTypeEnum; +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; /** * 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 array{stats?: BoltCypherStats}&array $response @@ -97,40 +146,87 @@ 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(function (array $response) use ($connection, $statement, $runStart, $resultAvailableAfter, &$summary) { - /** @var BoltCypherStats $response */ - $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( + 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['stats']['db'] ?? ''; + $summary = new ResultSummary( + $stats, + new DatabaseInfo($db), + new CypherList(), + null, + null, + $statement, + QueryTypeEnum::fromCounters($stats), + $resultAvailableAfter, + $resultConsumedAfter, + new ServerInfo( + $connection->getServerAddress(), + $connection->getProtocol(), + $connection->getServerAgent() + ) + ); + }); - $formattedResult = $this->formatter->formatBoltResult($meta, $result, $connection, $runStart, $resultAvailableAfter, $statement, $holder); + $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())); + } + + /** + * @param BoltMeta $meta + * + * @return CypherList> + */ + private function processBoltResult(array $meta, BoltResult $result, BoltConnection $connection, 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 + * + * @param BoltMeta $meta + * + * @return CypherMap + */ + private function formatRow(array $meta, array $result): CypherMap + { + /** @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/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 004be4d7..54bb1f5a 100644 --- a/src/Neo4j/Neo4jDriver.php +++ b/src/Neo4j/Neo4jDriver.php @@ -26,50 +26,34 @@ 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; /** * Driver for auto client-side routing. * - * @template T - * - * @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 - * : self - * ) - * * @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 +68,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/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 3d7c20fa..520e23c8 100644 --- a/src/ParameterHelper.php +++ b/src/ParameterHelper.php @@ -81,7 +81,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) ?? @@ -121,8 +121,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 []; } @@ -159,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 cd0ecd41..1168091f 100644 --- a/src/TypeCaster.php +++ b/src/TypeCaster.php @@ -117,7 +117,7 @@ public static function toArray(mixed $value): ?array $tbr = []; /** @var mixed $x */ foreach ($value as $x) { - /** @var mixed */ + /** @psalm-suppress MixedAssignment */ $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/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/ArrayList.php b/src/Types/ArrayList.php deleted file mode 100644 index b41fe92f..00000000 --- a/src/Types/ArrayList.php +++ /dev/null @@ -1,255 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use AppendIterator; -use ArrayIterator; -use Generator; - -use function is_array; -use function is_callable; -use function is_iterable; - -use Laudis\Neo4j\Exception\RuntimeTypeException; -use Laudis\Neo4j\TypeCaster; -use OutOfBoundsException; - -/** - * An immutable ordered sequence of items. - * - * @template TValue - * - * @extends AbstractCypherSequence - */ -class ArrayList extends AbstractCypherSequence -{ - /** - * @param iterable|callable():Generator $iterable - * - * @psalm-mutation-free - */ - public function __construct($iterable = []) - { - if (is_array($iterable)) { - $iterable = new ArrayIterator($iterable); - } - - $this->generator = static function () use ($iterable): Generator { - $i = 0; - /** @var Generator $it */ - $it = is_callable($iterable) ? $iterable() : $iterable; - foreach ($it as $value) { - yield $i => $value; - ++$i; - } - }; - } - - /** - * @template Value - * - * @param callable():(\Generator) $operation - * - * @return static - * - * @psalm-mutation-free - */ - protected function withOperation($operation): AbstractCypherSequence - { - /** @psalm-suppress UnsafeInstantiation */ - return new static($operation); - } - - /** - * Returns the first element in the sequence. - * - * @return TValue - */ - public function first() - { - foreach ($this as $value) { - return $value; - } - - throw new OutOfBoundsException('Cannot grab first element of an empty list'); - } - - /** - * Returns the last element in the sequence. - * - * @return TValue - */ - public function last() - { - if ($this->isEmpty()) { - throw new OutOfBoundsException('Cannot grab last element of an empty list'); - } - - $array = $this->toArray(); - - return $array[count($array) - 1]; - } - - /** - * @template NewValue - * - * @param iterable $values - * - * @psalm-suppress LessSpecificImplementedReturnType - * @psalm-suppress ImplementedReturnTypeMismatch - * - * @return static - * - * @psalm-mutation-free - */ - public function merge($values): ArrayList - { - return $this->withOperation(function () use ($values): Generator { - $iterator = new AppendIterator(); - - $iterator->append($this); - $iterator->append(new self($values)); - - yield from $iterator; - }); - } - - /** - * Gets the nth element in the list. - * - * @throws OutOfBoundsException - * - * @return TValue - */ - public function get(int $key) - { - return $this->offsetGet($key); - } - - public function getAsString(int $key): string - { - $value = $this->get($key); - $tbr = TypeCaster::toString($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'string'); - } - - return $tbr; - } - - public function getAsInt(int $key): int - { - $value = $this->get($key); - $tbr = TypeCaster::toInt($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'int'); - } - - return $tbr; - } - - public function getAsFloat(int $key): float - { - $value = $this->get($key); - $tbr = TypeCaster::toFloat($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'float'); - } - - return $tbr; - } - - public function getAsBool(int $key): bool - { - $value = $this->get($key); - $tbr = TypeCaster::toBool($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'bool'); - } - - return $tbr; - } - - /** - * @return null - */ - public function getAsNull(int $key) - { - /** @psalm-suppress UnusedMethodCall */ - $this->get($key); - - return TypeCaster::toNull(); - } - - /** - * @template U - * - * @param class-string $class - * - * @return U - */ - public function getAsObject(int $key, string $class): object - { - $value = $this->get($key); - $tbr = TypeCaster::toClass($value, $class); - if ($tbr === null) { - throw new RuntimeTypeException($value, $class); - } - - return $tbr; - } - - /** - * @return Map - */ - public function getAsMap(int $key): Map - { - $value = $this->get($key); - if (!is_iterable($value)) { - throw new RuntimeTypeException($value, Map::class); - } - - /** @psalm-suppress MixedArgumentTypeCoercion */ - return new Map($value); - } - - /** - * @return ArrayList - */ - public function getAsArrayList(int $key): ArrayList - { - $value = $this->get($key); - if (!is_iterable($value)) { - throw new RuntimeTypeException($value, self::class); - } - - /** @psalm-suppress MixedArgumentTypeCoercion */ - return new ArrayList($value); - } - - /** - * @template Value - * - * @param iterable $iterable - * - * @return static - * - * @pure - */ - public static function fromIterable(iterable $iterable): ArrayList - { - /** @psalm-suppress UnsafeInstantiation */ - return new static($iterable); - } -} diff --git a/src/Types/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 2a78832d..270354df 100644 --- a/src/Types/CypherMap.php +++ b/src/Types/CypherMap.php @@ -13,202 +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 { - /** @var mixed */ - $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 { - /** @var mixed */ - $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 1225f45c..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 array-key $offset * - * @param callable(TValue, TKey):void $callable - * - * @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; 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 deleted file mode 100644 index 2ca1c775..00000000 --- a/src/Types/Map.php +++ /dev/null @@ -1,510 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Types; - -use function array_key_exists; -use function array_key_last; - -use ArrayIterator; - -use function count; -use function func_num_args; - -use Generator; - -use function is_array; -use function is_callable; -use function is_iterable; - -use Laudis\Neo4j\Databags\Pair; -use Laudis\Neo4j\Exception\RuntimeTypeException; -use Laudis\Neo4j\TypeCaster; -use OutOfBoundsException; - -use function sprintf; - -use stdClass; - -/** - * An immutable ordered map of items. - * - * @template TValue - * - * @extends AbstractCypherSequence - */ -class Map extends AbstractCypherSequence -{ - /** - * @param iterable|callable():Generator $iterable - * - * @psalm-mutation-free - */ - public function __construct($iterable = []) - { - if (is_array($iterable)) { - $i = 0; - foreach ($iterable as $key => $value) { - if (!$this->isStringable($key)) { - $key = (string) $i; - } - /** @var string $key */ - $this->keyCache[] = $key; - /** @var TValue $value */ - $this->cache[$key] = $value; - ++$i; - } - /** @var ArrayIterator */ - $it = new ArrayIterator([]); - $this->generator = $it; - $this->generatorPosition = count($this->keyCache); - } else { - $this->generator = function () use ($iterable): Generator { - $i = 0; - /** @var Generator $it */ - $it = is_callable($iterable) ? $iterable() : $iterable; - /** @var mixed $key */ - foreach ($it as $key => $value) { - if ($this->isStringable($key)) { - yield (string) $key => $value; - } else { - yield (string) $i => $value; - } - ++$i; - } - }; - } - } - - /** - * @template Value - * - * @param callable():(\Generator) $operation - * - * @return static - * - * @psalm-mutation-free - */ - protected function withOperation($operation): Map - { - /** @psalm-suppress UnsafeInstantiation */ - return new static($operation); - } - - /** - * Returns the first pair in the map. - * - * @return Pair - */ - public function first(): Pair - { - foreach ($this as $key => $value) { - return new Pair($key, $value); - } - throw new OutOfBoundsException('Cannot grab first element of an empty map'); - } - - /** - * Returns the last pair in the map. - * - * @return Pair - */ - public function last(): Pair - { - $array = $this->toArray(); - if (count($array) === 0) { - throw new OutOfBoundsException('Cannot grab last element of an empty map'); - } - - $key = array_key_last($array); - - return new Pair($key, $array[$key]); - } - - /** - * Returns the pair at the nth position of the map. - * - * @return Pair - */ - public function skip(int $position): Pair - { - $i = 0; - foreach ($this as $key => $value) { - if ($i === $position) { - return new Pair($key, $value); - } - ++$i; - } - - throw new OutOfBoundsException(sprintf('Cannot skip to a pair at position: %s', $position)); - } - - /** - * Returns the keys in the map in order. - * - * @return ArrayList - * - * @psalm-suppress UnusedForeachValue - */ - public function keys(): ArrayList - { - return ArrayList::fromIterable((function () { - foreach ($this as $key => $value) { - yield $key; - } - })()); - } - - /** - * Returns the pairs in the map in order. - * - * @return ArrayList> - */ - public function pairs(): ArrayList - { - return ArrayList::fromIterable((function () { - foreach ($this as $key => $value) { - yield new Pair($key, $value); - } - })()); - } - - /** - * Create a new map sorted by keys. Natural ordering will be used if no comparator is provided. - * - * @param (callable(string, string):int)|null $comparator - * - * @return static - */ - public function ksorted(?callable $comparator = null): Map - { - return $this->withOperation(function () use ($comparator) { - $pairs = $this->pairs()->sorted(static function (Pair $x, Pair $y) use ($comparator) { - if ($comparator !== null) { - return $comparator($x->getKey(), $y->getKey()); - } - - return $x->getKey() <=> $y->getKey(); - }); - - foreach ($pairs as $pair) { - yield $pair->getKey() => $pair->getValue(); - } - }); - } - - /** - * Returns the values in the map in order. - * - * @return ArrayList - */ - public function values(): ArrayList - { - return ArrayList::fromIterable((function () { - yield from $this; - })()); - } - - /** - * Creates a new map using exclusive or on the keys. - * - * @param iterable $map - * - * @return static - */ - public function xor(iterable $map): Map - { - return $this->withOperation(function () use ($map) { - $map = Map::fromIterable($map); - foreach ($this as $key => $value) { - if (!$map->hasKey($key)) { - yield $key => $value; - } - } - - foreach ($map as $key => $value) { - if (!$this->hasKey($key)) { - yield $key => $value; - } - } - }); - } - - /** - * @template NewValue - * - * @param iterable $values - * - * @psalm-suppress LessSpecificImplementedReturnType - * - * @return self - * - * @psalm-mutation-free - */ - public function merge(iterable $values): Map - { - return $this->withOperation(function () use ($values) { - $tbr = $this->toArray(); - $values = Map::fromIterable($values); - - foreach ($values as $key => $value) { - $tbr[$key] = $value; - } - - yield from $tbr; - }); - } - - /** - * Creates a union of this and the provided map. The items in the original map take precedence. - * - * @param iterable $map - * - * @return static - */ - public function union(iterable $map): Map - { - return $this->withOperation(function () use ($map) { - $map = Map::fromIterable($map)->toArray(); - $x = $this->toArray(); - - yield from $x; - - foreach ($map as $key => $value) { - if (!array_key_exists($key, $x)) { - yield $key => $value; - } - } - }); - } - - /** - * Creates a new map from the existing one filtering the values based on the keys that don't exist in the provided map. - * - * @param iterable $map - * - * @return static - */ - public function intersect(iterable $map): Map - { - return $this->withOperation(function () use ($map) { - $map = Map::fromIterable($map)->toArray(); - foreach ($this as $key => $value) { - if (array_key_exists($key, $map)) { - yield $key => $value; - } - } - }); - } - - /** - * Creates a new map from the existing one filtering the values based on the keys that also exist in the provided map. - * - * @param iterable $map - * - * @return static - */ - public function diff(iterable $map): Map - { - return $this->withOperation(function () use ($map) { - $map = Map::fromIterable($map)->toArray(); - foreach ($this as $key => $value) { - if (!array_key_exists($key, $map)) { - yield $key => $value; - } - } - }); - } - - /** - * Gets the value with the provided key. If a default value is provided, it will return the default instead of throwing an error when the key does not exist. - * - * @template TDefault - * - * @param TDefault $default - * - * @throws OutOfBoundsException - * - * @return (func_num_args() is 1 ? TValue : TValue|TDefault) - */ - public function get(string $key, $default = null) - { - if (!$this->offsetExists($key)) { - if (func_num_args() === 1) { - throw new OutOfBoundsException(sprintf('Cannot get item in sequence with key: %s', $key)); - } - - return $default; - } - - return $this->offsetGet($key); - } - - public function jsonSerialize(): mixed - { - if ($this->isEmpty()) { - return new stdClass(); - } - - return parent::jsonSerialize(); - } - - public function getAsString(string $key, mixed $default = null): string - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - $tbr = TypeCaster::toString($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'string'); - } - - return $tbr; - } - - public function getAsInt(string $key, mixed $default = null): int - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - $tbr = TypeCaster::toInt($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'int'); - } - - return $tbr; - } - - public function getAsFloat(string $key, mixed $default = null): float - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - $tbr = TypeCaster::toFloat($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'float'); - } - - return $tbr; - } - - public function getAsBool(string $key, mixed $default = null): bool - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - $tbr = TypeCaster::toBool($value); - if ($tbr === null) { - throw new RuntimeTypeException($value, 'bool'); - } - - return $tbr; - } - - /** - * @return null - */ - public function getAsNull(string $key, mixed $default = null) - { - if (func_num_args() === 1) { - /** @psalm-suppress UnusedMethodCall */ - $this->get($key); - } - - return TypeCaster::toNull(); - } - - /** - * @template U - * - * @param class-string $class - * - * @return U - */ - public function getAsObject(string $key, string $class, mixed $default = null): object - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - $tbr = TypeCaster::toClass($value, $class); - if ($tbr === null) { - throw new RuntimeTypeException($value, $class); - } - - return $tbr; - } - - /** - * @return Map - */ - public function getAsMap(string $key, mixed $default = null): Map - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - - if (!is_iterable($value)) { - throw new RuntimeTypeException($value, self::class); - } - - return new Map($value); - } - - /** - * @return ArrayList - */ - public function getAsArrayList(string $key, mixed $default = null): ArrayList - { - if (func_num_args() === 1) { - $value = $this->get($key); - } else { - /** @var mixed */ - $value = $this->get($key, $default); - } - if (!is_iterable($value)) { - throw new RuntimeTypeException($value, ArrayList::class); - } - - return new ArrayList($value); - } - - /** - * @template Value - * - * @param iterable $iterable - * - * @return Map - */ - public static function fromIterable(iterable $iterable): Map - { - return new self($iterable); - } -} diff --git a/src/Types/Node.php b/src/Types/Node.php index b747781b..4c529fab 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 @@ -38,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 280a6679..f0b716a9 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 */ @@ -31,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 6f7a53a4..cde5fdb1 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 * @@ -35,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/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 a02e8e87..5a3d0401 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); @@ -339,7 +339,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); @@ -349,13 +348,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..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', [ @@ -48,8 +57,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 +134,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/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/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 <<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 <<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/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 848112b7..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()); } @@ -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..2a18b8e1 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 @@ -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()); } @@ -144,7 +145,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';