diff --git a/README.md b/README.md index 020a749c..48f55aed 100644 --- a/README.md +++ b/README.md @@ -308,21 +308,16 @@ composer require nyholm/psr7 nyholm/psr7-server kriswallsmith/buzz ## Result formats/hydration -In order to make the results of the bolt protocol and the http uniform, the driver provides result formatters (aka hydrators). The client is configurable with these formatters. You can even implement your own. +In order to make the results of the bolt protocol and the http uniform, the driver provides a summarizes the results. -The default formatter is the `\Laudis\Neo4j\Formatters\OGMFormatter`, which is explained extensively in [the result format section](#accessing-the-results). +The default formatter is the `\Laudis\Neo4j\Formatters\SummarizedResultFormatter`, which is explained extensively in [the result format section](#accessing-the-results). -The driver provides three formatters by default, which are all found in the Formatter namespace: - - `\Laudis\Neo4j\Formatter\BasicFormatter` which erases all the Cypher types and simply returns every value in the resulting map as a [scalar](https://www.php.net/manual/en/function.is-scalar.php), null or array value. - - `\Laudis\Neo4j\Formatter\OGMFormatter` which maps the cypher types to php types as explained [here](#accessing-the-results). - - `\Laudis\Neo4j\Formatter\SummarizedResultFormatter` which decorates any formatter and adds an extensive result summary. +`\Laudis\Neo4j\Formatter\SummarizedResultFormatter` adds an extensive result summary. The client builder provides an easy way to change the formatter: ```php -$client = \Laudis\Neo4j\ClientBuilder::create() - ->withFormatter(\Laudis\Neo4j\Formatter\SummarizedResultFormatter::create()) - ->build(); +$client = \Laudis\Neo4j\ClientBuilder::create()->build(); /** * The client will now return a result, decorated with a summary. @@ -339,8 +334,6 @@ $summary = $summarisedResult->getSummary(); $result = $summarisedResult->getResult(); ``` -In order to use a custom formatter, implement the `Laudis\Neo4j\Contracts\FormatterInterface` and provide it when using the client builder. - ## Concepts The driver API described [here](https://neo4j.com/docs/driver-manual/current/) is the main target of the driver. Because of this, the client is nothing more than a driver manager. The driver creates sessions. A session runs queries through a transaction. diff --git a/src/Basic/Client.php b/src/Basic/Client.php index afb6db66..d6df61b4 100644 --- a/src/Basic/Client.php +++ b/src/Basic/Client.php @@ -18,16 +18,9 @@ use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; -/** - * @implements ClientInterface> - */ final class Client implements ClientInterface { - /** - * @param ClientInterface> $client - */ public function __construct( private readonly ClientInterface $client ) {} diff --git a/src/Basic/Driver.php b/src/Basic/Driver.php index 7b73a876..f84f8eee 100644 --- a/src/Basic/Driver.php +++ b/src/Basic/Driver.php @@ -23,14 +23,9 @@ use Laudis\Neo4j\Types\CypherMap; use Psr\Http\Message\UriInterface; -/** - * @implements DriverInterface> - */ final class Driver implements DriverInterface { /** - * @param DriverInterface> $driver - * * @psalm-external-mutation-free */ public function __construct( @@ -52,7 +47,6 @@ public function verifyConnectivity(?SessionConfiguration $config = null): bool public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null): self { - /** @var DriverInterface> */ $driver = DriverFactory::create($uri, $configuration, $authenticate, SummarizedResultFormatter::create()); return new self($driver); diff --git a/src/Basic/Session.php b/src/Basic/Session.php index cb31416f..8052fc4a 100644 --- a/src/Basic/Session.php +++ b/src/Basic/Session.php @@ -21,14 +21,8 @@ use Laudis\Neo4j\Types\CypherList; use Laudis\Neo4j\Types\CypherMap; -/** - * @implements SessionInterface> - */ final class Session implements SessionInterface { - /** - * @param SessionInterface> $session - */ public function __construct( private readonly SessionInterface $session ) {} @@ -36,16 +30,13 @@ public function __construct( /** * @param iterable $statements * - * @return CypherList> + * @return CypherList */ public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList { return $this->session->runStatements($statements, $config); } - /** - * @return SummarizedResult - */ public function runStatement(Statement $statement, ?TransactionConfiguration $config = null): SummarizedResult { return $this->session->runStatement($statement, $config); @@ -53,8 +44,6 @@ public function runStatement(Statement $statement, ?TransactionConfiguration $co /** * @param iterable $parameters - * - * @return SummarizedResult */ public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null): SummarizedResult { diff --git a/src/Basic/UnmanagedTransaction.php b/src/Basic/UnmanagedTransaction.php index 233e582d..03a4631d 100644 --- a/src/Basic/UnmanagedTransaction.php +++ b/src/Basic/UnmanagedTransaction.php @@ -17,33 +17,21 @@ use Laudis\Neo4j\Databags\Statement; use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; -/** - * @implements UnmanagedTransactionInterface> - */ final class UnmanagedTransaction implements UnmanagedTransactionInterface { - /** - * @param UnmanagedTransactionInterface> $tsx - */ public function __construct( private readonly UnmanagedTransactionInterface $tsx ) {} /** * @param iterable $parameters - * - * @return SummarizedResult */ public function run(string $statement, iterable $parameters = []): SummarizedResult { return $this->tsx->run($statement, $parameters); } - /** - * @return SummarizedResult - */ public function runStatement(Statement $statement): SummarizedResult { return $this->tsx->runStatement($statement); @@ -52,7 +40,7 @@ public function runStatement(Statement $statement): SummarizedResult /** * @param iterable $statements * - * @return CypherList> + * @return CypherList */ public function runStatements(iterable $statements): CypherList { @@ -62,7 +50,7 @@ public function runStatements(iterable $statements): CypherList /** * @param iterable $statements * - * @return CypherList> + * @return CypherList */ public function commit(iterable $statements = []): CypherList { diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php index 93f3b3fa..40710426 100644 --- a/src/Bolt/BoltConnection.php +++ b/src/Bolt/BoltConnection.php @@ -27,13 +27,13 @@ use Laudis\Neo4j\Common\Neo4jLogger; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Contracts\ConnectionInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\BookmarkHolder; use Laudis\Neo4j\Databags\DatabaseInfo; use Laudis\Neo4j\Databags\Neo4jError; use Laudis\Neo4j\Enum\AccessMode; use Laudis\Neo4j\Enum\ConnectionProtocol; use Laudis\Neo4j\Exception\Neo4jException; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Types\CypherList; use Psr\Http\Message\UriInterface; use Psr\Log\LogLevel; @@ -42,7 +42,7 @@ /** * @implements ConnectionInterface * - * @psalm-import-type BoltMeta from FormatterInterface + * @psalm-import-type BoltMeta from SummarizedResultFormatter */ class BoltConnection implements ConnectionInterface { diff --git a/src/Bolt/BoltDriver.php b/src/Bolt/BoltDriver.php index 7c21c484..a3625a2f 100644 --- a/src/Bolt/BoltDriver.php +++ b/src/Bolt/BoltDriver.php @@ -23,50 +23,33 @@ use Laudis\Neo4j\Common\Uri; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Contracts\DriverInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Contracts\SessionInterface; use Laudis\Neo4j\Databags\DriverConfiguration; use Laudis\Neo4j\Databags\SessionConfiguration; -use Laudis\Neo4j\Formatter\OGMFormatter; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Psr\Http\Message\UriInterface; use Psr\Log\LogLevel; /** * Drives a singular bolt connections. * - * @template T - * - * @implements DriverInterface - * - * @psalm-import-type OGMResults from OGMFormatter + * @psalm-import-type OGMResults from SummarizedResultFormatter */ final class BoltDriver implements DriverInterface { /** - * @param FormatterInterface $formatter - * * @psalm-mutation-free */ public function __construct( private readonly UriInterface $parsedUrl, private readonly ConnectionPool $pool, - private readonly FormatterInterface $formatter + private readonly SummarizedResultFormatter $formatter ) {} /** - * @template U - * - * @param FormatterInterface $formatter - * - * @return ( - * func_num_args() is 5 - * ? self - * : self - * ) - * * @psalm-suppress MixedReturnTypeCoercion */ - public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null, ?FormatterInterface $formatter = null): self + public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null, ?SummarizedResultFormatter $formatter = null): self { if (is_string($uri)) { $uri = Uri::create($uri); @@ -80,7 +63,7 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co return new self( $uri, ConnectionPool::create($uri, $authenticate, $configuration, $semaphore), - $formatter ?? OGMFormatter::create(), + $formatter ?? SummarizedResultFormatter::create(), ); } diff --git a/src/Bolt/BoltResult.php b/src/Bolt/BoltResult.php index dd243b28..81c491ab 100644 --- a/src/Bolt/BoltResult.php +++ b/src/Bolt/BoltResult.php @@ -13,6 +13,7 @@ namespace Laudis\Neo4j\Bolt; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use function array_splice; use function count; @@ -21,10 +22,9 @@ use function in_array; use Iterator; -use Laudis\Neo4j\Contracts\FormatterInterface; /** - * @psalm-import-type BoltCypherStats from FormatterInterface + * @psalm-import-type BoltCypherStats from SummarizedResultFormatter * * @implements Iterator */ diff --git a/src/Bolt/BoltUnmanagedTransaction.php b/src/Bolt/BoltUnmanagedTransaction.php index 9a547551..80692275 100644 --- a/src/Bolt/BoltUnmanagedTransaction.php +++ b/src/Bolt/BoltUnmanagedTransaction.php @@ -14,14 +14,15 @@ 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; @@ -33,26 +34,19 @@ /** * 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, @@ -62,6 +56,8 @@ public function __construct( /** * @throws ClientException|Throwable + * + * @return CypherList */ public function commit(iterable $statements = []): CypherList { @@ -81,10 +77,8 @@ public function commit(iterable $statements = []): CypherList // Force the results to pull all the results. // After a commit, the connection will be in the ready state, making it impossible to use PULL - $tbr = $this->runStatements($statements)->each(static function ($list) { - if ($list instanceof AbstractCypherSequence) { - $list->preload(); - } + $tbr = $this->runStatements($statements)->each(static function (CypherList $list) { + $list->preload(); }); $this->connection->commit(); @@ -116,7 +110,7 @@ public function rollback(): void /** * @throws Throwable */ - public function run(string $statement, iterable $parameters = []) + public function run(string $statement, iterable $parameters = []): SummarizedResult { return $this->runStatement(new Statement($statement, $parameters)); } @@ -124,7 +118,7 @@ public function run(string $statement, iterable $parameters = []) /** * @throws Throwable */ - public function runStatement(Statement $statement) + public function runStatement(Statement $statement): SummarizedResult { $parameters = ParameterHelper::formatParameters($statement->getParameters(), $this->connection->getProtocol()); $start = microtime(true); @@ -162,10 +156,11 @@ public function runStatement(Statement $statement) /** * @throws Throwable + * + * @return CypherList */ public function runStatements(iterable $statements): CypherList { - /** @var list $tbr */ $tbr = []; foreach ($statements as $statement) { $tbr[] = $this->runStatement($statement); diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php index 8d895d59..137a44c3 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,14 @@ public function __construct( /** * @psalm-readonly */ - private readonly FormatterInterface $formatter + private readonly SummarizedResultFormatter $formatter ) { $this->bookmarkHolder = new BookmarkHolder(Bookmark::from($config->getBookmarks())); } + /** + * @return CypherList + */ public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList { $tbr = []; @@ -84,12 +83,12 @@ public function openTransaction(?iterable $statements = null, ?TransactionConfig return $this->beginTransaction($statements, $this->mergeTsxConfig($config)); } - public function runStatement(Statement $statement, ?TransactionConfiguration $config = null) + public function runStatement(Statement $statement, ?TransactionConfiguration $config = null): SummarizedResult { return $this->runStatements([$statement], $config)->first(); } - public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null) + public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null): SummarizedResult { return $this->runStatement(new Statement($statement, $parameters), $config); } @@ -133,7 +132,7 @@ public function beginTransaction(?iterable $statements = null, ?TransactionConfi } /** - * @return UnmanagedTransactionInterface + * @return UnmanagedTransactionInterface */ private function beginInstantTransaction( SessionConfiguration $config, diff --git a/src/Client.php b/src/Client.php index 9d862061..7e5aaa54 100644 --- a/src/Client.php +++ b/src/Client.php @@ -21,33 +21,30 @@ use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; use Laudis\Neo4j\Databags\SessionConfiguration; use Laudis\Neo4j\Databags\Statement; +use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Enum\AccessMode; use Laudis\Neo4j\Types\CypherList; /** * A collection of drivers with methods to run queries though them. - * - * @template ResultFormat - * - * @implements ClientInterface */ final class Client implements ClientInterface { /** - * @var array>> + * @var array> */ private array $boundTransactions = []; /** - * @var array> + * @var array */ private array $boundSessions = []; /** * @psalm-mutation-free * - * @param DriverSetupManager $driverSetups + * @param DriverSetupManager $driverSetups */ public function __construct( private readonly DriverSetupManager $driverSetups, @@ -70,12 +67,12 @@ public function getDefaultTransactionConfiguration(): TransactionConfiguration return $this->defaultTransactionConfiguration; } - public function run(string $statement, iterable $parameters = [], ?string $alias = null) + public function run(string $statement, iterable $parameters = [], ?string $alias = null): SummarizedResult { return $this->runStatement(Statement::create($statement, $parameters), $alias); } - public function runStatement(Statement $statement, ?string $alias = null) + public function runStatement(Statement $statement, ?string $alias = null): SummarizedResult { return $this->runStatements([$statement], $alias)->first(); } @@ -86,6 +83,7 @@ private function getRunner(?string $alias = null): TransactionInterface|SessionI if (array_key_exists($alias, $this->boundTransactions) && count($this->boundTransactions[$alias]) > 0) { + /** @psalm-suppress PossiblyNullArrayOffset */ return $this->boundTransactions[$alias][array_key_last($this->boundTransactions[$alias])]; } @@ -126,9 +124,6 @@ public function getDriver(?string $alias): DriverInterface return $this->driverSetups->getDriver($this->defaultSessionConfiguration, $alias); } - /** - * @return SessionInterface - */ private function startSession(?string $alias, SessionConfiguration $configuration): SessionInterface { return $this->getDriver($alias)->createSession($configuration); @@ -187,7 +182,7 @@ public function rollbackBoundTransaction(?string $alias = null, int $depth = 1): } /** - * @param callable(UnmanagedTransactionInterface): void $handler + * @param callable(UnmanagedTransactionInterface): void $handler */ private function popTransactions(callable $handler, ?string $alias = null, int $depth = 1): void { diff --git a/src/ClientBuilder.php b/src/ClientBuilder.php index 0ec4b497..791c8d5f 100644 --- a/src/ClientBuilder.php +++ b/src/ClientBuilder.php @@ -20,14 +20,12 @@ use Laudis\Neo4j\Common\Uri; use Laudis\Neo4j\Contracts\AuthenticateInterface; use Laudis\Neo4j\Contracts\ClientInterface; -use Laudis\Neo4j\Contracts\FormatterInterface; use Laudis\Neo4j\Databags\DriverConfiguration; use Laudis\Neo4j\Databags\DriverSetup; use Laudis\Neo4j\Databags\SessionConfiguration; use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Databags\TransactionConfiguration; use Laudis\Neo4j\Exception\UnsupportedScheme; -use Laudis\Neo4j\Formatter\OGMFormatter; use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Types\CypherMap; use Psr\Log\LoggerInterface; @@ -35,9 +33,7 @@ /** * Immutable factory for creating a client. * - * @template T - * - * @psalm-import-type OGMTypes from OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter */ final class ClientBuilder { @@ -46,7 +42,7 @@ final class ClientBuilder /** * @psalm-mutation-free * - * @param DriverSetupManager $driverSetups + * @param DriverSetupManager $driverSetups */ public function __construct( /** @psalm-readonly */ @@ -58,8 +54,6 @@ public function __construct( /** * Creates a client builder with default configurations and an OGMFormatter. - * - * @return ClientBuilder>> */ public static function create(?string $logLevel = null, ?LoggerInterface $logger = null): ClientBuilder { @@ -77,8 +71,6 @@ public static function create(?string $logLevel = null, ?LoggerInterface $logger /** * @psalm-mutation-free - * - * @return self */ public function withDriver(string $alias, string $url, ?AuthenticateInterface $authentication = null, ?int $priority = 0): self { @@ -91,8 +83,6 @@ public function withDriver(string $alias, string $url, ?AuthenticateInterface $a /** * @psalm-external-mutation-free - * - * @return self */ private function withParsedUrl(string $alias, Uri $uri, AuthenticateInterface $authentication, int $priority): self { @@ -111,8 +101,6 @@ private function withParsedUrl(string $alias, Uri $uri, AuthenticateInterface $a /** * Sets the default connection to the given alias. * - * @return self - * * @psalm-mutation-free */ public function withDefaultDriver(string $alias): self @@ -124,15 +112,9 @@ public function withDefaultDriver(string $alias): self } /** - * @template U - * - * @param FormatterInterface $formatter - * - * @return self - * * @psalm-mutation-free */ - public function withFormatter(FormatterInterface $formatter): self + public function withFormatter(SummarizedResultFormatter $formatter): self { return new self( $this->defaultSessionConfig, @@ -142,8 +124,6 @@ public function withFormatter(FormatterInterface $formatter): self } /** - * @return ClientInterface - * * @psalm-mutation-free */ public function build(): ClientInterface @@ -156,8 +136,6 @@ public function build(): ClientInterface } /** - * @return self - * * @psalm-mutation-free */ public function withDefaultDriverConfiguration(DriverConfiguration $config): self @@ -170,8 +148,6 @@ public function withDefaultDriverConfiguration(DriverConfiguration $config): sel } /** - * @return self - * * @psalm-mutation-free */ public function withDefaultSessionConfiguration(SessionConfiguration $config): self @@ -183,8 +159,6 @@ public function withDefaultSessionConfiguration(SessionConfiguration $config): s } /** - * @return self - * * @psalm-mutation-free */ public function withDefaultTransactionConfiguration(TransactionConfiguration $config): self diff --git a/src/Common/DriverSetupManager.php b/src/Common/DriverSetupManager.php index bb2639d2..28b5646b 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,26 +36,21 @@ 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 SummarizedResultFormatter $formatter, private DriverConfiguration $configuration ) {} @@ -101,9 +96,6 @@ public function hasDriver(string $alias): bool return array_key_exists($alias, $this->driverSetups); } - /** - * @return DriverInterface - */ public function getDriver(SessionConfiguration $config, ?string $alias = null): DriverInterface { $alias ??= $this->decideAlias($alias); @@ -193,7 +185,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/TransactionHelper.php b/src/Common/TransactionHelper.php index a9be078e..f756c5fb 100644 --- a/src/Common/TransactionHelper.php +++ b/src/Common/TransactionHelper.php @@ -24,10 +24,9 @@ final class TransactionHelper /** * @template U - * @template T * - * @param callable():UnmanagedTransactionInterface $tsxFactory - * @param callable(TransactionInterface):U $tsxHandler + * @param callable():UnmanagedTransactionInterface $tsxFactory + * @param callable(TransactionInterface):U $tsxHandler * * @return U */ diff --git a/src/Contracts/ClientInterface.php b/src/Contracts/ClientInterface.php index 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/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/SummarizedResult.php b/src/Databags/SummarizedResult.php index e72249a2..846966eb 100644 --- a/src/Databags/SummarizedResult.php +++ b/src/Databags/SummarizedResult.php @@ -20,20 +20,18 @@ /** * A result containing the values and the summary. * - * @template TValue - * - * @extends CypherList + * @extends CypherList */ final class SummarizedResult extends CypherList { private ?ResultSummary $summary = null; /** - * @param iterable|callable():Generator $iterable + * @param iterable|callable():Generator $iterable * * @psalm-mutation-free */ - public function __construct(?ResultSummary &$summary, $iterable = []) + public function __construct(?ResultSummary &$summary, iterable|callable $iterable = []) { parent::__construct($iterable); $this->summary = &$summary; @@ -42,9 +40,9 @@ public function __construct(?ResultSummary &$summary, $iterable = []) /** * @template Value * - * @param callable():(\Generator) $operation + * @param callable():(Generator) $operation * - * @return static + * @return static * * @psalm-mutation-free */ diff --git a/src/DriverFactory.php b/src/DriverFactory.php index 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..43213190 100644 --- a/src/Formatter/Specialised/BoltOGMTranslator.php +++ b/src/Formatter/Specialised/BoltOGMTranslator.php @@ -26,7 +26,7 @@ use Bolt\protocol\v1\structures\Relationship as BoltRelationship; use Bolt\protocol\v1\structures\Time as BoltTime; use Bolt\protocol\v1\structures\UnboundRelationship as BoltUnboundRelationship; -use Laudis\Neo4j\Formatter\OGMFormatter; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use Laudis\Neo4j\Types\Abstract3DPoint; use Laudis\Neo4j\Types\AbstractPoint; use Laudis\Neo4j\Types\Cartesian3DPoint; @@ -51,7 +51,7 @@ /** * Translates Bolt objects to Driver Types. * - * @psalm-import-type OGMTypes from OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter * * @psalm-immutable * diff --git a/src/Formatter/SummarizedResultFormatter.php b/src/Formatter/SummarizedResultFormatter.php index 27b29842..215e2d7f 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,37 +27,85 @@ 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 ) {} /** @@ -101,30 +149,31 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection { /** @var ResultSummary|null $summary */ $summary = null; - $result->addFinishedCallback(function (array $response) use ($connection, $statement, $runStart, $resultAvailableAfter, &$summary) { - /** @var BoltCypherStats $response */ - $stats = $this->formatBoltStats($response); - $resultConsumedAfter = microtime(true) - $runStart; - $db = $response['db'] ?? ''; - $summary = new ResultSummary( - $stats, - new DatabaseInfo($db), - new CypherList(), - null, - null, - $statement, - QueryTypeEnum::fromCounters($stats), - $resultAvailableAfter, - $resultConsumedAfter, - new ServerInfo( - $connection->getServerAddress(), - $connection->getProtocol(), - $connection->getServerAgent() - ) - ); - }); + $result->addFinishedCallback( + /** @param {array{stats?: BoltCypherStats}&array} $response */ + function (mixed $response) use ($connection, $statement, $runStart, $resultAvailableAfter, &$summary) { + $stats = $this->formatBoltStats($response); + $resultConsumedAfter = microtime(true) - $runStart; + $db = $response['db'] ?? ''; + $summary = new ResultSummary( + $stats, + new DatabaseInfo($db), + new CypherList(), + null, + null, + $statement, + QueryTypeEnum::fromCounters($stats), + $resultAvailableAfter, + $resultConsumedAfter, + new ServerInfo( + $connection->getServerAddress(), + $connection->getProtocol(), + $connection->getServerAgent() + ) + ); + }); - $formattedResult = $this->formatter->formatBoltResult($meta, $result, $connection, $runStart, $resultAvailableAfter, $statement, $holder); + $formattedResult = $this->processBoltResult($meta, $result, $connection, $runStart, $resultAvailableAfter, $statement, $holder); /** * @psalm-suppress MixedArgument @@ -133,4 +182,45 @@ public function formatBoltResult(array $meta, BoltResult $result, BoltConnection */ return (new SummarizedResult($summary, $formattedResult))->withCacheLimit($result->getFetchSize()); } + + /** + * @param BoltMeta $meta + * + * @return CypherList> + */ + private function processBoltResult(array $meta, BoltResult $result, BoltConnection $connection, float $runStart, float $resultAvailableAfter, Statement $statement, BookmarkHolder $holder): CypherList + { + $tbr = (new CypherList(function () use ($result, $meta) { + foreach ($result as $row) { + yield $this->formatRow($meta, $row); + } + }))->withCacheLimit($result->getFetchSize()); + + $connection->subscribeResult($tbr); + $result->addFinishedCallback(function (array $response) use ($holder) { + if (array_key_exists('bookmark', $response) && is_string($response['bookmark'])) { + $holder->setBookmark(new Bookmark([$response['bookmark']])); + } + }); + + return $tbr; + } + + /** + * @psalm-mutation-free + */ + 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/Neo4jDriver.php b/src/Neo4j/Neo4jDriver.php index 004be4d7..768d8bf6 100644 --- a/src/Neo4j/Neo4jDriver.php +++ b/src/Neo4j/Neo4jDriver.php @@ -26,50 +26,33 @@ 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 +67,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/ParameterHelper.php b/src/ParameterHelper.php index 3d7c20fa..0cf857ea 100644 --- a/src/ParameterHelper.php +++ b/src/ParameterHelper.php @@ -78,6 +78,8 @@ public static function asMap(iterable $iterable): CypherMap /** * @return iterable|scalar|stdClass|IStructure|null + * + * @param \DateTime|array|object|stdClass $value */ public static function asParameter( mixed $value, diff --git a/src/Types/AbstractCypherSequence.php b/src/Types/AbstractCypherSequence.php index 1225f45c..8b82e344 100644 --- a/src/Types/AbstractCypherSequence.php +++ b/src/Types/AbstractCypherSequence.php @@ -553,6 +553,8 @@ public function preload(): void /** * @psalm-mutation-free + * + * @param {int|string|object} $key */ protected function isStringable(mixed $key): bool { diff --git a/src/Types/AbstractPropertyObject.php b/src/Types/AbstractPropertyObject.php index 3b37ee9c..673e6cf9 100644 --- a/src/Types/AbstractPropertyObject.php +++ b/src/Types/AbstractPropertyObject.php @@ -15,11 +15,12 @@ use BadMethodCallException; use Laudis\Neo4j\Contracts\HasPropertiesInterface; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use function sprintf; /** - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter * * @template PropertyTypes * @template ObjectTypes diff --git a/src/Types/CypherMap.php b/src/Types/CypherMap.php index 2a78832d..5037299e 100644 --- a/src/Types/CypherMap.php +++ b/src/Types/CypherMap.php @@ -33,9 +33,10 @@ final class CypherMap extends Map public function getAsCypherMap(string $key, mixed $default = null): CypherMap { if (func_num_args() === 1) { + /** @var mixed $value */ $value = $this->get($key); } else { - /** @var mixed */ + /** @var mixed $value */ $value = $this->get($key, $default); } $tbr = TypeCaster::toCypherMap($value); @@ -52,9 +53,10 @@ public function getAsCypherMap(string $key, mixed $default = null): CypherMap public function getAsCypherList(string $key, mixed $default = null): CypherList { if (func_num_args() === 1) { + /** @var mixed $value */ $value = $this->get($key); } else { - /** @var mixed */ + /** @var mixed $value */ $value = $this->get($key, $default); } $tbr = TypeCaster::toCypherList($value); diff --git a/src/Types/Node.php b/src/Types/Node.php index b747781b..0c660714 100644 --- a/src/Types/Node.php +++ b/src/Types/Node.php @@ -14,13 +14,14 @@ namespace Laudis\Neo4j\Types; use Laudis\Neo4j\Exception\PropertyDoesNotExistException; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use function sprintf; /** * A Node class representing a Node in cypher. * - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter * * @psalm-immutable * @psalm-immutable diff --git a/src/Types/Relationship.php b/src/Types/Relationship.php index 280a6679..0528d58d 100644 --- a/src/Types/Relationship.php +++ b/src/Types/Relationship.php @@ -13,10 +13,12 @@ namespace Laudis\Neo4j\Types; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; + /** * A Relationship class representing a Relationship in cypher. * - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter * * @psalm-immutable */ diff --git a/src/Types/UnboundRelationship.php b/src/Types/UnboundRelationship.php index 6f7a53a4..1148c960 100644 --- a/src/Types/UnboundRelationship.php +++ b/src/Types/UnboundRelationship.php @@ -14,13 +14,14 @@ namespace Laudis\Neo4j\Types; use Laudis\Neo4j\Exception\PropertyDoesNotExistException; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; use function sprintf; /** * A relationship without any nodes attached to it. * - * @psalm-import-type OGMTypes from \Laudis\Neo4j\Formatter\OGMFormatter + * @psalm-import-type OGMTypes from SummarizedResultFormatter * * @psalm-immutable * diff --git a/tests/Integration/BoltDriverIntegrationTest.php b/tests/Integration/BoltDriverIntegrationTest.php index 2b449cf5..31987e9a 100644 --- a/tests/Integration/BoltDriverIntegrationTest.php +++ b/tests/Integration/BoltDriverIntegrationTest.php @@ -16,7 +16,6 @@ use Bolt\error\ConnectException; use Exception; use Laudis\Neo4j\Bolt\BoltDriver; -use Laudis\Neo4j\Databags\SummarizedResult; use Laudis\Neo4j\Neo4j\Neo4jDriver; use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest; use Throwable; @@ -84,7 +83,6 @@ public function testBookmarkUpdates(): void $this->assertTrue($bookmark->isEmpty()); $previousBookmark = $bookmark; - /** @var SummarizedResult $result */ $result = $session->run('MATCH (x) RETURN x'); $result->preload(); @@ -93,7 +91,6 @@ public function testBookmarkUpdates(): void $this->assertNotEquals($previousBookmark->values(), $bookmark->values()); $previousBookmark = $bookmark; - /** @var SummarizedResult $result */ $result = $session->run('CREATE (x:Node)'); $result->preload(); diff --git a/tests/Integration/ClientIntegrationTest.php b/tests/Integration/ClientIntegrationTest.php index 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..a0e812d5 100644 --- a/tests/Integration/ComplexQueryTest.php +++ b/tests/Integration/ComplexQueryTest.php @@ -48,8 +48,7 @@ public function testValidListParameterHelper(): void public function testMergeTransactionFunction(): void { $this->expectException(Neo4jException::class); - $this->getSession()->writeTransaction(static fn (TSX $tsx) => /** @psalm-suppress ALL */ -$tsx->run('MERGE (x {y: "z"}:X) return x')->first() + $this->getSession()->writeTransaction(static fn (TSX $tsx): string => $tsx->run('MERGE (x {y: "z"}:X) return x')->first() ->getAsMap('x') ->getAsString('y')); } @@ -126,7 +125,7 @@ public function testPath(): void self::assertEquals( [['attribute' => 'xy'], ['attribute' => 'yz']], /** @psalm-suppress MissingClosureReturnType */ - $result->getAsCypherList('y')->map(static fn ($r) => /** + $result->getAsCypherList('y')->map(static fn (mixed $r): array => /** * @psalm-suppress MixedMethodCall * * @var array diff --git a/tests/Integration/OGMFormatterIntegrationTest.php b/tests/Integration/OGMFormatterIntegrationTest.php deleted file mode 100644 index 185809b7..00000000 --- a/tests/Integration/OGMFormatterIntegrationTest.php +++ /dev/null @@ -1,451 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Laudis\Neo4j\Tests\Integration; - -use function compact; - -use DateInterval; - -use function json_encode; - -use Laudis\Neo4j\Contracts\PointInterface; -use Laudis\Neo4j\Contracts\TransactionInterface; -use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest; -use Laudis\Neo4j\Types\CartesianPoint; -use Laudis\Neo4j\Types\CypherList; -use Laudis\Neo4j\Types\CypherMap; -use Laudis\Neo4j\Types\Date; -use Laudis\Neo4j\Types\DateTime; -use Laudis\Neo4j\Types\Duration; -use Laudis\Neo4j\Types\LocalDateTime; -use Laudis\Neo4j\Types\LocalTime; -use Laudis\Neo4j\Types\Node; -use Laudis\Neo4j\Types\Path; -use Laudis\Neo4j\Types\Relationship; -use Laudis\Neo4j\Types\Time; - -use function range; -use function sprintf; - -/** - * @psalm-suppress MixedArrayAccess - */ -final class OGMFormatterIntegrationTest extends EnvironmentAwareIntegrationTest -{ - public function testNull(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN null as x')); - - self::assertNull($results->first()->get('x')); - } - - public function testList(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN range(5, 15) as list, range(16, 35) as list2')); - - $list = $results->first()->get('list'); - $list2 = $results->first()->get('list2'); - - self::assertInstanceOf(CypherList::class, $list); - self::assertInstanceOf(CypherList::class, $list2); - self::assertEquals(range(5, 15), $list->toArray()); - self::assertEquals(range(16, 35), $list2->toArray()); - self::assertEquals(json_encode(range(5, 15), JSON_THROW_ON_ERROR), json_encode($list, JSON_THROW_ON_ERROR)); - self::assertEquals(json_encode(range(16, 35), JSON_THROW_ON_ERROR), json_encode($list2, JSON_THROW_ON_ERROR)); - } - - public function testMap(): void - { - $map = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN {a: "b", c: "d"} as map')->first()->get('map')); - self::assertInstanceOf(CypherMap::class, $map); - $array = $map->toArray(); - ksort($array); - self::assertEquals(['a' => 'b', 'c' => 'd'], $array); - } - - public function testBoolean(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN true as bool1, false as bool2')); - - self::assertEquals(1, $results->count()); - self::assertIsBool($results->first()->get('bool1')); - self::assertIsBool($results->first()->get('bool2')); - } - - public function testInteger(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<count()); - self::assertEquals(1, $results[0]['x.num']); - self::assertEquals(2, $results[1]['x.num']); - self::assertEquals(3, $results[2]['x.num']); - } - - public function testFloat(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN 0.1 AS float')); - - self::assertIsFloat($results->first()->get('float')); - } - - public function testString(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN "abc" AS string')); - - self::assertIsString($results->first()->get('string')); - } - - public function testDate(): void - { - $results = $this->getSession()->transaction(function (TransactionInterface $tsx) { - $query = $this->articlesQuery(); - $query .= 'RETURN article.datePublished as published_at'; - - return $tsx->run($query); - }); - - self::assertEquals(3, $results->count()); - - $publishedAt = $results[0]['published_at']; - self::assertInstanceOf(Date::class, $publishedAt); - self::assertEquals(18048, $publishedAt->getDays()); - self::assertEquals( - json_encode(['days' => 18048], JSON_THROW_ON_ERROR), - json_encode($publishedAt, JSON_THROW_ON_ERROR)); - self::assertEquals(18048, $publishedAt->days); - - self::assertInstanceOf(Date::class, $results[1]['published_at']); - self::assertEquals(18049, $results[1]['published_at']->getDays()); - self::assertEquals( - json_encode(['days' => 18049], JSON_THROW_ON_ERROR), - json_encode($results[1]['published_at'], JSON_THROW_ON_ERROR)); - - self::assertInstanceOf(Date::class, $results[2]['published_at']); - self::assertEquals(18742, $results[2]['published_at']->getDays()); - self::assertEquals( - json_encode(['days' => 18742], JSON_THROW_ON_ERROR), - json_encode($results[2]['published_at'], JSON_THROW_ON_ERROR)); - } - - public function testTime(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN time("12:00:00.000000000") AS time')); - - $time = $results->first()->get('time'); - self::assertInstanceOf(Time::class, $time); - self::assertEquals(12.0 * 60 * 60 * 1_000_000_000, $time->getNanoSeconds()); - self::assertEquals(12.0 * 60 * 60 * 1_000_000_000, $time->nanoSeconds); - } - - public function testLocalTime(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN localtime("12") AS time')); - - /** @var LocalTime $time */ - $time = $results->first()->get('time'); - self::assertInstanceOf(LocalTime::class, $time); - self::assertEquals(43_200_000_000_000, $time->getNanoseconds()); - - $results = $this->getSession()->run('RETURN localtime("09:23:42.000") AS time', []); - - /** @var LocalTime $time */ - $time = $results->first()->get('time'); - self::assertInstanceOf(LocalTime::class, $time); - self::assertEquals(33_822_000_000_000, $time->getNanoseconds()); - self::assertEquals(33_822_000_000_000, $time->nanoseconds); - } - - public function testDateTime(): void - { - $results = $this->getSession()->transaction(function (TransactionInterface $tsx) { - $query = $this->articlesQuery(); - $query .= 'RETURN article.created as created_at'; - - return $tsx->run($query); - }); - - self::assertEquals(3, $results->count()); - - $createdAt = $results[0]['created_at']; - self::assertInstanceOf(DateTime::class, $createdAt); - if ($createdAt->isLegacy()) { - self::assertEquals(1_559_414_432, $createdAt->getSeconds()); - } else { - self::assertEquals(1_559_410_832, $createdAt->getSeconds()); - } - - self::assertEquals(142_000_000, $createdAt->getNanoseconds()); - self::assertEquals(3600, $createdAt->getTimeZoneOffsetSeconds()); - self::assertEquals(142_000_000, $createdAt->getNanoseconds()); - self::assertEquals(3600, $createdAt->getTimeZoneOffsetSeconds()); - - if ($createdAt->isLegacy()) { - self::assertEquals('{"seconds":1559414432,"nanoseconds":142000000,"tzOffsetSeconds":3600}', json_encode($createdAt, JSON_THROW_ON_ERROR)); - } else { - self::assertEquals('{"seconds":1559410832,"nanoseconds":142000000,"tzOffsetSeconds":3600}', json_encode($createdAt, JSON_THROW_ON_ERROR)); - } - } - - public function testLocalDateTime(): void - { - $result = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN localdatetime() as local')->first()->get('local')); - - self::assertInstanceOf(LocalDateTime::class, $result); - $date = $result->toDateTime(); - self::assertEquals($result->getSeconds(), $date->getTimestamp()); - } - - public function testDuration(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<count()); - self::assertEquals(new Duration(0, 14, 58320, 0), $results[0]['aDuration']); - $duration = $results[1]['aDuration']; - self::assertInstanceOf(Duration::class, $duration); - self::assertEquals(new Duration(5, 1, 43200, 0), $duration); - self::assertEquals(5, $duration->getMonths()); - self::assertEquals(1, $duration->getDays()); - self::assertEquals(43200, $duration->getSeconds()); - self::assertEquals(0, $duration->getNanoseconds()); - self::assertEquals(new Duration(0, 22, 71509, 500_000_000), $results[2]['aDuration']); - self::assertEquals(new Duration(0, 17, 43200, 0), $results[3]['aDuration']); - self::assertEquals(new Duration(0, 0, 91, 123_456_789), $results[4]['aDuration']); - self::assertEquals(new Duration(0, 0, 91, 123_456_789), $results[5]['aDuration']); - - self::assertEquals(5, $duration->getMonths()); - self::assertEquals(1, $duration->getDays()); - self::assertEquals(43200, $duration->getSeconds()); - self::assertEquals(0, $duration->getNanoseconds()); - $interval = new DateInterval(sprintf('P%dM%dDT%dS', 5, 1, 43200)); - self::assertEquals($interval, $duration->toDateInterval()); - self::assertEquals('{"months":5,"days":1,"seconds":43200,"nanoseconds":0}', json_encode($duration, JSON_THROW_ON_ERROR)); - } - - public function testPoint(): void - { - $result = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run('RETURN point({x: 3, y: 4}) AS point')); - self::assertInstanceOf(CypherList::class, $result); - $row = $result->first(); - self::assertInstanceOf(CypherMap::class, $row); - $point = $row->get('point'); - - self::assertInstanceOf(CartesianPoint::class, $point); - self::assertEquals(3.0, $point->getX()); - self::assertEquals(4.0, $point->getY()); - self::assertEquals('cartesian', $point->getCrs()); - self::assertGreaterThan(0, $point->getSrid()); - self::assertEquals(3.0, $point->x); - self::assertEquals(4.0, $point->y); - self::assertEquals('cartesian', $point->crs); - self::assertGreaterThan(0, $point->srid); - self::assertEquals( - json_encode([ - 'x' => 3, - 'y' => 4, - 'crs' => 'cartesian', - 'srid' => 7203, - ], JSON_THROW_ON_ERROR), - json_encode($point, JSON_THROW_ON_ERROR) - ); - } - - public function testNode(): void - { - $uuid = 'cc60fd69-a92b-47f3-9674-2f27f3437d66'; - $email = 'a@b.c'; - $type = 'pepperoni'; - - $arguments = compact('email', 'uuid', 'type'); - - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run( - 'MERGE (u:User{email: $email})-[:LIKES]->(p:Food:Pizza {type: $type}) ON CREATE SET u.uuid=$uuid RETURN u, p', - $arguments - )); - - self::assertEquals(1, $results->count()); - - /** @var Node $u */ - $u = $results[0]['u']; - self::assertInstanceOf(Node::class, $u); - self::assertEquals(['User'], $u->getLabels()->toArray()); - self::assertEquals($email, $u->getProperties()['email']); - self::assertEquals($uuid, $u->getProperties()['uuid']); - self::assertEquals($email, $u->email); - self::assertEquals($uuid, $u->uuid); - self::assertEquals( - json_encode([ - 'id' => $u->getId(), - 'labels' => $u->getLabels()->jsonSerialize(), - 'properties' => $u->getProperties()->jsonSerialize(), - ], JSON_THROW_ON_ERROR), - json_encode($u, JSON_THROW_ON_ERROR)); - - /** @var Node $p */ - $p = $results[0]['p']; - self::assertInstanceOf(Node::class, $p); - self::assertEquals(['Food', 'Pizza'], $p->getLabels()->toArray()); - self::assertEquals($type, $p->getProperties()['type']); - self::assertEquals( - json_encode([ - 'id' => $p->getId(), - 'labels' => $p->getLabels()->jsonSerialize(), - 'properties' => $p->getProperties()->jsonSerialize(), - ], JSON_THROW_ON_ERROR), - json_encode($p, JSON_THROW_ON_ERROR) - ); - } - - public function testRelationship(): void - { - $result = $this->getSession()->transaction(static function (TransactionInterface $tsx) { - $tsx->run('MATCH (n) DETACH DELETE n'); - - return $tsx->run('MERGE (x:X {x: 1}) - [xy:XY {x: 1, y: 1}] -> (y:Y {y: 1}) RETURN xy')->first()->get('xy'); - }); - - self::assertInstanceOf(Relationship::class, $result); - self::assertEquals('XY', $result->getType()); - self::assertEquals(['x' => 1, 'y' => 1], $result->getProperties()->toArray()); - self::assertEquals(1, $result->x); - self::assertEquals(1, $result->y); - self::assertEquals( - json_encode([ - 'id' => $result->getId(), - 'type' => $result->getType(), - 'properties' => $result->getProperties(), - 'startNodeId' => $result->getStartNodeId(), - 'endNodeId' => $result->getEndNodeId(), - ], JSON_THROW_ON_ERROR), - json_encode($result, JSON_THROW_ON_ERROR) - ); - } - - public function testPath(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<<'CYPHER' -MERGE (b:Node {x:$x}) - [:HasNode {attribute: $xy}] -> (:Node {y:$y}) - [:HasNode {attribute: $yz}] -> (:Node {z:$z}) -WITH b -MATCH (x:Node) - [y:HasNode*2] -> (z:Node) -RETURN x, y, z -CYPHER, ['x' => 'x', 'xy' => 'xy', 'y' => 'y', 'yz' => 'yz', 'z' => 'z'])); - - self::assertEquals(1, $results->count()); - } - - public function testPath2(): void - { - $results = $this->getSession()->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<<'CYPHER' -CREATE path = (a:Node {x:$x}) - [b:HasNode {attribute: $xy}] -> (c:Node {y:$y}) - [d:HasNode {attribute: $yz}] -> (e:Node {z:$z}) -RETURN path -CYPHER, ['x' => 'x', 'xy' => 'xy', 'y' => 'y', 'yz' => 'yz', 'z' => 'z'])); - - self::assertEquals(1, $results->count()); - $path = $results->first()->get('path'); - - self::assertInstanceOf(Path::class, $path); - self::assertCount(2, $path->getRelationships()); - self::assertCount(3, $path->getNodes()); - - self::assertEquals(['x' => 'x'], $path->getNodes()->get(0)->getProperties()->toArray()); - self::assertEquals(['y' => 'y'], $path->getNodes()->get(1)->getProperties()->toArray()); - self::assertEquals(['z' => 'z'], $path->getNodes()->get(2)->getProperties()->toArray()); - self::assertEquals(['attribute' => 'xy'], $path->getRelationships()->get(0)->getProperties()->toArray()); - self::assertEquals(['attribute' => 'yz'], $path->getRelationships()->get(1)->getProperties()->toArray()); - } - - public function testPropertyTypes(): void - { - $result = $this->getSession(['neo4j', 'bolt'])->transaction(static fn (TransactionInterface $tsx) => $tsx->run(<<first()->get('a'); - - self::assertInstanceOf(Node::class, $node); - self::assertInstanceOf(PointInterface::class, $node->thePoint); - self::assertInstanceOf(CypherList::class, $node->theList); - self::assertInstanceOf(Date::class, $node->theDate); - self::assertInstanceOf(DateTime::class, $node->theDateTime); - self::assertInstanceOf(Duration::class, $node->theDuration); - self::assertInstanceOf(LocalDateTime::class, $node->theLocalDateTime); - self::assertInstanceOf(LocalTime::class, $node->theLocalTime); - self::assertInstanceOf(Time::class, $node->theTime); - } - - private function articlesQuery(): string - { - return <<