diff --git a/composer.json b/composer.json index 9c446f8..32202f9 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "symfony/stopwatch": "^6.4", "symfony/test-pack": "^1.1", "symfony/twig-bundle": "^5.4 || ^6.0 || ^7.0", + "symfony/uid": "^6.4", "symfony/web-profiler-bundle": "^5.4 || ^6.0 || ^7.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0", "vimeo/psalm": "^5.15.0" diff --git a/config/services.php b/config/services.php index 59dd13f..e758418 100644 --- a/config/services.php +++ b/config/services.php @@ -2,13 +2,22 @@ use Laudis\Neo4j\Basic\Driver; use Laudis\Neo4j\Basic\Session; +use Laudis\Neo4j\Common\DriverSetupManager; use Laudis\Neo4j\Contracts\ClientInterface; use Laudis\Neo4j\Contracts\DriverInterface; use Laudis\Neo4j\Contracts\SessionInterface; use Laudis\Neo4j\Contracts\TransactionInterface; -use Neo4j\Neo4jBundle\ClientFactory; +use Laudis\Neo4j\Databags\DriverConfiguration; +use Laudis\Neo4j\Databags\SessionConfiguration; +use Laudis\Neo4j\Databags\TransactionConfiguration; +use Laudis\Neo4j\Formatter\SummarizedResultFormatter; +use Neo4j\Neo4jBundle\Builders\ClientBuilder; +use Neo4j\Neo4jBundle\Decorators\SymfonyClient; +use Neo4j\Neo4jBundle\EventHandler; use Neo4j\Neo4jBundle\EventListener\Neo4jProfileListener; -use Neo4j\Neo4jBundle\SymfonyClient; +use Neo4j\Neo4jBundle\Factories\ClientFactory; +use Neo4j\Neo4jBundle\Factories\StopwatchEventNameFactory; +use Neo4j\Neo4jBundle\Factories\SymfonyDriverFactory; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use function Symfony\Component\DependencyInjection\Loader\Configurator\service; @@ -16,10 +25,14 @@ return static function (ContainerConfigurator $configurator) { $services = $configurator->services(); - $services->set('neo4j.client_factory', ClientFactory::class) - ->args([ - service('neo4j.event_handler'), - ]); + $services->set('neo4j.client_factory', ClientFactory::class); + + $services->set(DriverConfiguration::class, DriverConfiguration::class) + ->factory([DriverConfiguration::class, 'default']); + $services->set(SessionConfiguration::class, SessionConfiguration::class); + $services->set(TransactionConfiguration::class, TransactionConfiguration::class); + $services->set(ClientBuilder::class, ClientBuilder::class) + ->autowire(); $services->set('neo4j.client', SymfonyClient::class) ->factory([service('neo4j.client_factory'), 'create']) @@ -39,6 +52,24 @@ ->share(false) ->public(); + $services->set(SymfonyDriverFactory::class, SymfonyDriverFactory::class) + ->arg('$handler', service(EventHandler::class)) + ->arg('$uuidFactory', service('uuid.factory')->nullOnInvalid()); + + $services->set(StopwatchEventNameFactory::class, StopwatchEventNameFactory::class); + $services->set(EventHandler::class, EventHandler::class) + ->arg('$dispatcher', service('event_dispatcher')->nullOnInvalid()) + ->arg('$stopwatch', service('debug.stopwatch')->nullOnInvalid()) + ->arg('$nameFactory', service(StopwatchEventNameFactory::class)); + + $services->set(StopwatchEventNameFactory::class); + + $services->set(DriverSetupManager::class, DriverSetupManager::class) + ->arg('$formatter', service(SummarizedResultFormatter::class)) + ->arg('$configuration', service(DriverConfiguration::class)); + $services->set(SummarizedResultFormatter::class, SummarizedResultFormatter::class) + ->factory([SummarizedResultFormatter::class, 'create']); + $services->alias(ClientInterface::class, 'neo4j.client'); $services->alias(DriverInterface::class, 'neo4j.driver'); $services->alias(SessionInterface::class, 'neo4j.session'); diff --git a/src/Builders/ClientBuilder.php b/src/Builders/ClientBuilder.php new file mode 100644 index 0000000..2f229e7 --- /dev/null +++ b/src/Builders/ClientBuilder.php @@ -0,0 +1,99 @@ + $driverSetups + */ + public function __construct( + private SessionConfiguration $defaultSessionConfig, + private TransactionConfiguration $defaultTransactionConfig, + private DriverSetupManager $driverSetups, + private readonly SymfonyDriverFactory $driverFactory, + ) { + } + + public function withDriver(string $alias, string $url, ?AuthenticateInterface $authentication = null, ?int $priority = 0): self + { + $uri = Uri::create($url); + + $authentication ??= Authenticate::fromUrl($uri, $this->driverSetups->getLogger()); + + return $this->withParsedUrl($alias, $uri, $authentication, $priority ?? 0); + } + + private function withParsedUrl(string $alias, Uri $uri, AuthenticateInterface $authentication, int $priority): self + { + $scheme = $uri->getScheme(); + + if (!in_array($scheme, self::SUPPORTED_SCHEMES, true)) { + throw UnsupportedScheme::make($scheme, self::SUPPORTED_SCHEMES); + } + + $tbr = clone $this; + $tbr->driverSetups = $this->driverSetups->withSetup(new DriverSetup($uri, $authentication), $alias, $priority); + + return $tbr; + } + + public function withDefaultDriver(string $alias): self + { + $tbr = clone $this; + $tbr->driverSetups = $this->driverSetups->withDefault($alias); + + return $tbr; + } + + public function build(): SymfonyClient + { + return new SymfonyClient( + driverSetups: $this->driverSetups, + defaultSessionConfiguration: $this->defaultSessionConfig, + defaultTransactionConfiguration: $this->defaultTransactionConfig, + factory: $this->driverFactory + ); + } + + public function withDefaultDriverConfiguration(DriverConfiguration $config): self + { + $tbr = clone $this; + + $tbr->driverSetups = $tbr->driverSetups->withDriverConfiguration($config); + + return $tbr; + } + + public function withDefaultSessionConfiguration(SessionConfiguration $config): self + { + $tbr = clone $this; + $tbr->defaultSessionConfig = $config; + + return $tbr; + } + + public function withDefaultTransactionConfiguration(TransactionConfiguration $config): self + { + $tbr = clone $this; + $tbr->defaultTransactionConfig = $config; + + return $tbr; + } +} diff --git a/src/Collector/Neo4jDataCollector.php b/src/Collector/Neo4jDataCollector.php index 3e7531e..8543887 100644 --- a/src/Collector/Neo4jDataCollector.php +++ b/src/Collector/Neo4jDataCollector.php @@ -51,7 +51,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep 'time' => $x['time'], 'timestamp' => $x['timestamp'], 'result' => [ - 'statement' => $x['statement']->toArray(), + 'statement' => $x['statement']?->toArray(), ], 'exception' => [ 'code' => $x['exception']->getErrors()[0]->getCode(), diff --git a/src/Decorators/SymfonyClient.php b/src/Decorators/SymfonyClient.php new file mode 100644 index 0000000..37207ba --- /dev/null +++ b/src/Decorators/SymfonyClient.php @@ -0,0 +1,243 @@ +> + * + * @psalm-external-mutation-free + * + * @psalm-suppress ImpureMethodCall + */ +class SymfonyClient implements ClientInterface +{ + /** + * @var array> + */ + private array $boundTransactions = []; + + /** + * @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 SymfonyDriverFactory $factory, + ) { + } + + public function getDefaultSessionConfiguration(): SessionConfiguration + { + return $this->defaultSessionConfiguration; + } + + public function getDefaultTransactionConfiguration(): TransactionConfiguration + { + return $this->defaultTransactionConfiguration; + } + + 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): SummarizedResult + { + return $this->runStatements([$statement], $alias)->first(); + } + + private function getRunner(?string $alias = null): SymfonyTransaction|SymfonySession + { + $alias ??= $this->driverSetups->getDefaultAlias(); + + if ( + array_key_exists($alias, $this->boundTransactions) + && count($this->boundTransactions[$alias]) > 0 + ) { + return $this->boundTransactions[$alias][array_key_last($this->boundTransactions[$alias])]; + } + + return $this->getSession($alias); + } + + private function getSession(?string $alias = null): SymfonySession + { + $alias ??= $this->driverSetups->getDefaultAlias(); + + if (array_key_exists($alias, $this->boundSessions)) { + return $this->boundSessions[$alias]; + } + + return $this->boundSessions[$alias] = $this->startSession($alias, $this->defaultSessionConfiguration); + } + + public function runStatements(iterable $statements, ?string $alias = null): CypherList + { + $runner = $this->getRunner($alias); + if ($runner instanceof SessionInterface) { + return $runner->runStatements($statements, $this->defaultTransactionConfiguration); + } + + return $runner->runStatements($statements); + } + + public function beginTransaction(?iterable $statements = null, ?string $alias = null, ?TransactionConfiguration $config = null): SymfonyTransaction + { + $session = $this->getSession($alias); + $config = $this->getTsxConfig($config); + + return $session->beginTransaction($statements, $config); + } + + public function getDriver(?string $alias): SymfonyDriver + { + return $this->factory->createDriver( + new Driver($this->driverSetups->getDriver($this->defaultSessionConfiguration, $alias)), + $alias ?? $this->driverSetups->getDefaultAlias(), + '', + ); + } + + private function startSession(?string $alias, SessionConfiguration $configuration): SymfonySession + { + return $this->factory->createSession( + new Driver($this->driverSetups->getDriver($this->defaultSessionConfiguration, $alias)), + $configuration, + $alias ?? $this->driverSetups->getDefaultAlias(), + '', + ); + } + + /** + * @template HandlerResult + * + * @param callable(SymfonyTransaction):HandlerResult $tsxHandler + * + * @return HandlerResult + */ + public function writeTransaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null): mixed + { + if ($this->defaultSessionConfiguration->getAccessMode() === AccessMode::WRITE()) { + $session = $this->getSession($alias); + } else { + $sessionConfig = $this->defaultSessionConfiguration->withAccessMode(AccessMode::WRITE()); + $session = $this->startSession($alias, $sessionConfig); + } + + return $session->writeTransaction($tsxHandler, $this->getTsxConfig($config)); + } + + /** + * @template HandlerResult + * + * @param callable(SymfonyTransaction):HandlerResult $tsxHandler + * + * @return HandlerResult + */ + public function readTransaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null): mixed + { + if ($this->defaultSessionConfiguration->getAccessMode() === AccessMode::READ()) { + $session = $this->getSession($alias); + } else { + $sessionConfig = $this->defaultSessionConfiguration->withAccessMode(AccessMode::WRITE()); + $session = $this->startSession($alias, $sessionConfig); + } + + return $session->readTransaction($tsxHandler, $this->getTsxConfig($config)); + } + + /** + * @template HandlerResult + * + * @param callable(SymfonyTransaction):HandlerResult $tsxHandler + * + * @return HandlerResult + */ + public function transaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null) + { + return $this->writeTransaction($tsxHandler, $alias, $config); + } + + public function verifyConnectivity(?string $driver = null): bool + { + return $this->driverSetups->verifyConnectivity($this->defaultSessionConfiguration, $driver); + } + + public function hasDriver(string $alias): bool + { + return $this->driverSetups->hasDriver($alias); + } + + public function bindTransaction(?string $alias = null, ?TransactionConfiguration $config = null): void + { + $alias ??= $this->driverSetups->getDefaultAlias(); + + $this->boundTransactions[$alias] ??= []; + $this->boundTransactions[$alias][] = $this->beginTransaction(null, $alias, $config); + } + + public function rollbackBoundTransaction(?string $alias = null, int $depth = 1): void + { + $this->popTransactions(static fn (SymfonyTransaction $tsx) => $tsx->rollback(), $alias, $depth); + } + + /** + * @param callable(SymfonyTransaction): void $handler + * + * @psalm-suppress ImpureFunctionCall + */ + private function popTransactions(callable $handler, ?string $alias = null, int $depth = 1): void + { + $alias ??= $this->driverSetups->getDefaultAlias(); + + if (!array_key_exists($alias, $this->boundTransactions)) { + return; + } + + while (0 !== count($this->boundTransactions[$alias]) && 0 !== $depth) { + $tsx = array_pop($this->boundTransactions[$alias]); + $handler($tsx); + --$depth; + } + } + + public function commitBoundTransaction(?string $alias = null, int $depth = 1): void + { + $this->popTransactions(static fn (UnmanagedTransactionInterface $tsx) => $tsx->commit(), $alias, $depth); + } + + private function getTsxConfig(?TransactionConfiguration $config): TransactionConfiguration + { + if (null !== $config) { + return $this->defaultTransactionConfiguration->merge($config); + } + + return $this->defaultTransactionConfiguration; + } +} diff --git a/src/Decorators/SymfonyDriver.php b/src/Decorators/SymfonyDriver.php new file mode 100644 index 0000000..ea2d699 --- /dev/null +++ b/src/Decorators/SymfonyDriver.php @@ -0,0 +1,43 @@ +> + * + * @psalm-immutable + * + * @psalm-suppress ImpureMethodCall + */ +class SymfonyDriver implements DriverInterface +{ + public function __construct( + private readonly Driver $driver, + private readonly SymfonyDriverFactory $factory, + private readonly string $alias, + private readonly string $schema, + ) { + } + + public function createSession(?SessionConfiguration $config = null): SymfonySession + { + return $this->factory->createSession($this->driver, $config, $this->alias, $this->schema); + } + + public function verifyConnectivity(?SessionConfiguration $config = null): bool + { + return $this->driver->verifyConnectivity(); + } + + public function closeConnections(): void + { + $this->driver->closeConnections(); + } +} diff --git a/src/Decorators/SymfonySession.php b/src/Decorators/SymfonySession.php new file mode 100644 index 0000000..9cbef39 --- /dev/null +++ b/src/Decorators/SymfonySession.php @@ -0,0 +1,113 @@ +> + */ +class SymfonySession implements SessionInterface +{ + public function __construct( + private readonly Session $session, + private readonly EventHandler $handler, + private readonly SymfonyDriverFactory $factory, + private readonly string $alias, + private readonly string $schema, + ) { + } + + public function runStatements(iterable $statements, ?TransactionConfiguration $config = null): CypherList + { + $tbr = []; + foreach ($statements as $statement) { + $tbr[] = $this->runStatement($statement); + } + + return CypherList::fromIterable($tbr); + } + + public function runStatement(Statement $statement, ?TransactionConfiguration $config = null) + { + return $this->handler->handleQuery( + runHandler: fn (Statement $statement) => $this->session->runStatement($statement), + statement: $statement, + alias: $this->alias, + scheme: $this->schema, + transactionId: null + ); + } + + public function run(string $statement, iterable $parameters = [], ?TransactionConfiguration $config = null) + { + return $this->runStatement(new Statement($statement, $parameters)); + } + + public function beginTransaction(?iterable $statements = null, ?TransactionConfiguration $config = null): SymfonyTransaction + { + return $this->factory->createTransaction( + session: $this->session, + config: $config, + alias: $this->alias, + schema: $this->schema + ); + } + + /** + * @template HandlerResult + * + * @param callable(SymfonyTransaction):HandlerResult $tsxHandler + * + * @return HandlerResult + * + * @psalm-suppress ArgumentTypeCoercion + */ + public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) + { + return TransactionHelper::retry( + fn () => $this->beginTransaction(config: $config), + $tsxHandler + ); + } + + /** + * @template HandlerResult + * + * @param callable(SymfonyTransaction):HandlerResult $tsxHandler + * + * @return HandlerResult + */ + public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null) + { + // TODO: create read transaction here. + return $this->writeTransaction($tsxHandler, $config); + } + + /** + * @template HandlerResult + * + * @param callable(SymfonyTransaction):HandlerResult $tsxHandler + * + * @return HandlerResult + */ + public function transaction(callable $tsxHandler, ?TransactionConfiguration $config = null) + { + return $this->writeTransaction($tsxHandler, $config); + } + + public function getLastBookmark(): Bookmark + { + return $this->session->getLastBookmark(); + } +} diff --git a/src/SymfonyTransaction.php b/src/Decorators/SymfonyTransaction.php similarity index 64% rename from src/SymfonyTransaction.php rename to src/Decorators/SymfonyTransaction.php index 603b732..f6d1379 100644 --- a/src/SymfonyTransaction.php +++ b/src/Decorators/SymfonyTransaction.php @@ -2,13 +2,15 @@ declare(strict_types=1); -namespace Neo4j\Neo4jBundle; +namespace Neo4j\Neo4jBundle\Decorators; use Laudis\Neo4j\Contracts\UnmanagedTransactionInterface; use Laudis\Neo4j\Databags\Statement; use Laudis\Neo4j\Databags\SummarizedResult; +use Laudis\Neo4j\Enum\TransactionState; use Laudis\Neo4j\Types\CypherList; use Laudis\Neo4j\Types\CypherMap; +use Neo4j\Neo4jBundle\EventHandler; /** * @implements UnmanagedTransactionInterface> @@ -21,7 +23,9 @@ class SymfonyTransaction implements UnmanagedTransactionInterface public function __construct( private readonly UnmanagedTransactionInterface $tsx, private readonly EventHandler $handler, - private readonly ?string $alias, + private readonly string $alias, + private readonly string $scheme, + private readonly string $transactionId, ) { } @@ -32,10 +36,11 @@ public function run(string $statement, iterable $parameters = []): SummarizedRes public function runStatement(Statement $statement): SummarizedResult { - return $this->handler->handle(fn ($statement) => $this->tsx->runStatement($statement), + return $this->handler->handleQuery(fn (Statement $statement) => $this->tsx->runStatement($statement), $statement, $this->alias, - null + $this->scheme, + $this->transactionId ); } @@ -54,16 +59,28 @@ public function runStatements(iterable $statements): CypherList public function commit(iterable $statements = []): CypherList { - $tbr = $this->runStatements($statements); + $results = $this->runStatements($statements); - $this->tsx->commit(); + $this->handler->handleTransactionAction( + TransactionState::COMMITTED, + $this->transactionId, + fn () => $this->tsx->commit(), + $this->alias, + $this->scheme, + ); - return $tbr; + return $results; } public function rollback(): void { - $this->tsx->rollback(); + $this->handler->handleTransactionAction( + TransactionState::ROLLED_BACK, + $this->transactionId, + fn () => $this->tsx->commit(), + $this->alias, + $this->scheme, + ); } public function isRolledBack(): bool diff --git a/src/DependencyInjection/Neo4jExtension.php b/src/DependencyInjection/Neo4jExtension.php index 3f03457..a19e6c1 100644 --- a/src/DependencyInjection/Neo4jExtension.php +++ b/src/DependencyInjection/Neo4jExtension.php @@ -6,6 +6,7 @@ use Laudis\Neo4j\Contracts\DriverInterface; use Laudis\Neo4j\Contracts\SessionInterface; +use Neo4j\Neo4jBundle\Builders\ClientBuilder; use Neo4j\Neo4jBundle\Collector\Neo4jDataCollector; use Neo4j\Neo4jBundle\EventHandler; use Neo4j\Neo4jBundle\EventListener\Neo4jProfileListener; @@ -41,22 +42,23 @@ public function load(array $configs, ContainerBuilder $container): ContainerBuil ->setArgument(1, $defaultAlias); $container->getDefinition('neo4j.client_factory') - ->setArgument(1, $mergedConfig['default_driver_config'] ?? null) - ->setArgument(2, $mergedConfig['default_session_config'] ?? null) - ->setArgument(3, $mergedConfig['default_transaction_config'] ?? null) - ->setArgument(4, $mergedConfig['drivers'] ?? []) - ->setArgument(5, $mergedConfig['default_driver'] ?? null) - ->setArgument(6, new Reference(ClientInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) + ->setArgument('$driverConfig', $mergedConfig['default_driver_config'] ?? null) + ->setArgument('$sessionConfig', $mergedConfig['default_session_config'] ?? null) + ->setArgument('$transactionConfig', $mergedConfig['default_transaction_config'] ?? null) + ->setArgument('$connections', $mergedConfig['drivers'] ?? []) + ->setArgument('$defaultDriver', $mergedConfig['default_driver'] ?? null) + ->setArgument('$builder', new Reference(ClientBuilder::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) + ->setArgument('$client', new Reference(ClientInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) ->setArgument( - 7, + '$streamFactory', new Reference(StreamFactoryInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE) ) ->setArgument( - 8, + '$requestFactory', new Reference(RequestFactoryInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE) ) - ->setArgument(9, $mergedConfig['min_log_level'] ?? null) - ->setArgument(10, new Reference(LoggerInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) + ->setArgument('$logLevel', $mergedConfig['min_log_level'] ?? null) + ->setArgument('$logger', new Reference(LoggerInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) ->setAbstract(false); $container->getDefinition('neo4j.driver') diff --git a/src/Event/FailureEvent.php b/src/Event/FailureEvent.php index 50ab49f..1aa2966 100644 --- a/src/Event/FailureEvent.php +++ b/src/Event/FailureEvent.php @@ -16,13 +16,19 @@ class FailureEvent extends Event public function __construct( private readonly ?string $alias, - private readonly Statement $statement, + private readonly ?Statement $statement, private readonly Neo4jException $exception, private readonly \DateTimeInterface $time, private readonly ?string $scheme, + private readonly ?string $transactionId, ) { } + public function getStatement(): ?Statement + { + return $this->statement; + } + public function getException(): Neo4jException { return $this->exception; @@ -49,13 +55,13 @@ public function getAlias(): ?string return $this->alias; } - public function getStatement(): Statement + public function getScheme(): ?string { - return $this->statement; + return $this->scheme; } - public function getScheme(): ?string + public function getTransactionId(): ?string { - return $this->scheme; + return $this->transactionId; } } diff --git a/src/Event/PostRunEvent.php b/src/Event/PostRunEvent.php index 38c38da..26750f4 100644 --- a/src/Event/PostRunEvent.php +++ b/src/Event/PostRunEvent.php @@ -16,6 +16,7 @@ public function __construct( private readonly ResultSummary $result, private readonly \DateTimeInterface $time, private readonly ?string $scheme, + private readonly ?string $transactionId, ) { } @@ -38,4 +39,9 @@ public function getScheme(): ?string { return $this->scheme; } + + public function getTransactionId(): ?string + { + return $this->transactionId; + } } diff --git a/src/Event/PreRunEvent.php b/src/Event/PreRunEvent.php index 8772a15..360a942 100644 --- a/src/Event/PreRunEvent.php +++ b/src/Event/PreRunEvent.php @@ -16,6 +16,7 @@ public function __construct( private readonly Statement $statement, private readonly \DateTimeInterface $time, private readonly ?string $scheme, + private readonly ?string $transactionId, ) { } @@ -39,4 +40,9 @@ public function getScheme(): ?string { return $this->scheme; } + + public function getTransactionId(): ?string + { + return $this->transactionId; + } } diff --git a/src/Event/Transaction/PostTransactionBeginEvent.php b/src/Event/Transaction/PostTransactionBeginEvent.php new file mode 100644 index 0000000..317f28c --- /dev/null +++ b/src/Event/Transaction/PostTransactionBeginEvent.php @@ -0,0 +1,16 @@ +dispatcher = $dispatcher; } @@ -36,60 +36,193 @@ public function __construct( /** * @template T * - * @param callable(Statement):SummarizedResult $runHandler + * @param callable(Statement):T $runHandler * - * @return SummarizedResult + * @return T */ - public function handle(callable $runHandler, Statement $statement, ?string $alias, ?string $scheme): SummarizedResult + public function handleQuery(callable $runHandler, Statement $statement, string $alias, string $scheme, ?string $transactionId): SummarizedResult { - $stopWatchName = sprintf('neo4j.%s.query', $alias ?? $this->alias); - if (null === $this->dispatcher) { - $this->stopwatch?->start($stopWatchName); - $result = $runHandler($statement); - $this->stopwatch?->stop($stopWatchName); + $stopwatchName = $this->nameFactory->createQueryEventName($alias, $transactionId); - return $result; - } + $time = new \DateTimeImmutable(); + $event = new PreRunEvent( + alias: $alias, + statement: $statement, + time: $time, + scheme: $scheme, + transactionId: $transactionId + ); + + $this->dispatcher?->dispatch($event, PreRunEvent::EVENT_ID); + + $runHandler = static fn (): mixed => $runHandler($statement); + $result = $this->handleAction( + runHandler: $runHandler, + alias: $alias, + scheme: $scheme, + stopwatchName: $stopwatchName, + transactionId: $transactionId, + statement: $statement + ); + + $event = new PostRunEvent( + alias: $alias, + result: $result->getSummary(), + time: $time, + scheme: $scheme, + transactionId: $transactionId + ); + + $this->dispatcher?->dispatch( + $event, + PostRunEvent::EVENT_ID + ); + + return $result; + } - /** @noinspection PhpUnhandledExceptionInspection */ - $time = new \DateTimeImmutable('now', new \DateTimeZone(date_default_timezone_get())); - $this->dispatcher->dispatch(new PreRunEvent($alias, $statement, $time, $scheme), PreRunEvent::EVENT_ID); + /** + * @template T + * + * @param callable():T $runHandler + * + * @return T + */ + public function handleTransactionAction( + TransactionState $nextTransactionState, + string $transactionId, + callable $runHandler, + string $alias, + string $scheme, + ): mixed { + $stopWatchName = $this->nameFactory->createTransactionEventName($alias, $transactionId, $nextTransactionState); + + [ + 'preEvent' => $preEvent, + 'preEventId' => $preEventId, + 'postEvent' => $postEvent, + 'postEventId' => $postEventId, + ] = $this->createPreAndPostEventsAndIds( + nextTransactionState: $nextTransactionState, + alias: $alias, + scheme: $scheme, + transactionId: $transactionId + ); + $this->dispatcher?->dispatch($preEvent, $preEventId); + $result = $this->handleAction(runHandler: $runHandler, alias: $alias, scheme: $scheme, stopwatchName: $stopWatchName, transactionId: $transactionId, statement: null); + $this->dispatcher?->dispatch($postEvent, $postEventId); + + return $result; + } + + /** + * @template T + * + * @param callable():T $runHandler + * + * @return T + */ + private function handleAction(callable $runHandler, string $alias, string $scheme, string $stopwatchName, ?string $transactionId, ?Statement $statement): mixed + { try { - $this->stopwatch?->start($stopWatchName); - $tbr = $runHandler($statement); - $this->stopwatch?->stop($stopWatchName); - $this->dispatcher->dispatch( - new PostRunEvent($alias ?? $this->alias, $tbr->getSummary(), $time, $scheme), - PostRunEvent::EVENT_ID - ); + $this->stopwatch?->start($stopwatchName, 'database'); + $result = $runHandler(); + $this->stopwatch?->stop($stopwatchName); + + return $result; } catch (Neo4jException $e) { - $this->stopwatch?->stop($stopWatchName); - /** @noinspection PhpUnhandledExceptionInspection */ - $time = new \DateTimeImmutable('now', new \DateTimeZone(date_default_timezone_get())); - $event = new FailureEvent($alias ?? $this->alias, $statement, $e, $time, $scheme); - $event = $this->dispatcher->dispatch($event, FailureEvent::EVENT_ID); - - if ($event->shouldThrowException()) { - throw $e; - } - - $summary = new ResultSummary( - new SummaryCounters(), - new DatabaseInfo('n/a'), - new CypherList([]), - null, - null, - $statement, - QueryTypeEnum::READ_ONLY(), - 0, - 0, - new ServerInfo(Uri::create(''), ConnectionProtocol::BOLT_V5(), 'n/a'), + $this->stopwatch?->stop($stopwatchName); + $event = new FailureEvent( + alias: $alias, + statement: $statement, + exception: $e, + time: new \DateTimeImmutable('now'), + scheme: $scheme, + transactionId: $transactionId ); - $tbr = new SummarizedResult($summary); + $this->dispatcher?->dispatch($event, FailureEvent::EVENT_ID); + + throw $e; } + } + + /** + * @return array{'preEvent': object, 'preEventId': string, 'postEvent': object, 'postEventId': string} + */ + private function createPreAndPostEventsAndIds( + TransactionState $nextTransactionState, + string $alias, + string $scheme, + string $transactionId, + ): array { + [$preEvent, $preEventId] = match ($nextTransactionState) { + TransactionState::ACTIVE => [ + new PreTransactionBeginEvent( + alias: $alias, + time: new \DateTimeImmutable(), + scheme: $scheme, + transactionId: $transactionId, + ), + PreTransactionBeginEvent::EVENT_ID, + ], + TransactionState::ROLLED_BACK => [ + new PreTransactionRollbackEvent( + alias: $alias, + time: new \DateTimeImmutable(), + scheme: $scheme, + transactionId: $transactionId, + ), + PreTransactionRollbackEvent::EVENT_ID, + ], + TransactionState::COMMITTED => [ + new PreTransactionCommitEvent( + alias: $alias, + time: new \DateTimeImmutable(), + scheme: $scheme, + transactionId: $transactionId, + ), + PreTransactionCommitEvent::EVENT_ID, + ], + TransactionState::TERMINATED => throw new \UnexpectedValueException('TERMINATED is not a valid transaction state at this point'), + }; + [$postEvent, $postEventId] = match ($nextTransactionState) { + TransactionState::ACTIVE => [ + new PostTransactionBeginEvent( + alias: $alias, + time: new \DateTimeImmutable(), + scheme: $scheme, + transactionId: $transactionId, + ), + PostTransactionBeginEvent::EVENT_ID, + ], + TransactionState::ROLLED_BACK => [ + new PostTransactionRollbackEvent( + alias: $alias, + time: new \DateTimeImmutable(), + scheme: $scheme, + transactionId: $transactionId, + ), + PostTransactionRollbackEvent::EVENT_ID, + ], + TransactionState::COMMITTED => [ + new PostTransactionCommitEvent( + alias: $alias, + time: new \DateTimeImmutable(), + scheme: $scheme, + transactionId: $transactionId, + ), + PostTransactionCommitEvent::EVENT_ID, + ], + TransactionState::TERMINATED => throw new \UnexpectedValueException('TERMINATED is not a valid transaction state at this point'), + }; - return $tbr; + return [ + 'preEvent' => $preEvent, + 'preEventId' => $preEventId, + 'postEvent' => $postEvent, + 'postEventId' => $postEventId, + ]; } } diff --git a/src/EventListener/Neo4jProfileListener.php b/src/EventListener/Neo4jProfileListener.php index e5c91d7..e9a8a29 100644 --- a/src/EventListener/Neo4jProfileListener.php +++ b/src/EventListener/Neo4jProfileListener.php @@ -28,7 +28,7 @@ final class Neo4jProfileListener implements EventSubscriberInterface, ResetInter /** * @var list $connections */ public function __construct( - private EventHandler $eventHandler, - private ?array $driverConfig, - private ?array $sessionConfiguration, - private ?array $transactionConfiguration, - private array $connections, - private ?string $defaultDriver, - private ?ClientInterface $client, - private ?StreamFactoryInterface $streamFactory, - private ?RequestFactoryInterface $requestFactory, - private ?string $logLevel, - private ?LoggerInterface $logger, + private readonly ?array $driverConfig, + private readonly ?array $sessionConfig, + private readonly ?array $transactionConfig, + private readonly array $connections, + private readonly ?string $defaultDriver, + private readonly ?ClientInterface $client, + private readonly ?StreamFactoryInterface $streamFactory, + private readonly ?RequestFactoryInterface $requestFactory, + private readonly ?string $logLevel, + private readonly ?LoggerInterface $logger, + private ClientBuilder $builder, ) { } public function create(): SymfonyClient { - /** @var ClientBuilder> $builder */ - $builder = ClientBuilder::create(); - if (null !== $this->driverConfig) { - $builder = $builder->withDefaultDriverConfiguration( + $this->builder = $this->builder->withDefaultDriverConfiguration( $this->makeDriverConfig($this->logLevel, $this->logger) ); } - if (null !== $this->sessionConfiguration) { - $builder = $builder->withDefaultSessionConfiguration($this->makeSessionConfig()); + if (null !== $this->sessionConfig) { + $this->builder = $this->builder->withDefaultSessionConfiguration($this->makeSessionConfig()); } - if (null !== $this->transactionConfiguration) { - $builder = $builder->withDefaultTransactionConfiguration($this->makeTransactionConfig()); + if (null !== $this->transactionConfig) { + $this->builder = $this->builder->withDefaultTransactionConfiguration($this->makeTransactionConfig()); } foreach ($this->connections as $connection) { - $builder = $builder->withDriver( + $this->builder = $this->builder->withDriver( $connection['alias'], $connection['dsn'], $this->createAuth($connection['authentication'] ?? null, $connection['dsn']), @@ -83,10 +79,10 @@ public function create(): SymfonyClient } if (null !== $this->defaultDriver) { - $builder = $builder->withDefaultDriver($this->defaultDriver); + $this->builder = $this->builder->withDefaultDriver($this->defaultDriver); } - return new SymfonyClient($builder->build(), $this->eventHandler); + return $this->builder->build(); } private function makeDriverConfig(?string $logLevel = null, ?LoggerInterface $logger = null): DriverConfiguration @@ -122,9 +118,9 @@ private function makeDriverConfig(?string $logLevel = null, ?LoggerInterface $lo private function makeSessionConfig(): SessionConfiguration { return new SessionConfiguration( - database: $this->sessionConfiguration['database'] ?? null, - fetchSize: $this->sessionConfiguration['fetch_size'] ?? null, - accessMode: match ($this->sessionConfiguration['access_mode'] ?? null) { + database: $this->sessionConfig['database'] ?? null, + fetchSize: $this->sessionConfig['fetch_size'] ?? null, + accessMode: match ($this->sessionConfig['access_mode'] ?? null) { 'write', null => AccessMode::WRITE(), 'read' => AccessMode::READ(), }, @@ -134,7 +130,7 @@ private function makeSessionConfig(): SessionConfiguration private function makeTransactionConfig(): TransactionConfiguration { return new TransactionConfiguration( - timeout: $this->transactionConfiguration['timeout'] ?? null + timeout: $this->transactionConfig['timeout'] ?? null ); } diff --git a/src/Factories/StopwatchEventNameFactory.php b/src/Factories/StopwatchEventNameFactory.php new file mode 100644 index 0000000..a7002dd --- /dev/null +++ b/src/Factories/StopwatchEventNameFactory.php @@ -0,0 +1,41 @@ +nextTransactionStateToAction($nextTransactionState) + ); + } + + private function nextTransactionStateToAction(TransactionState $state): string + { + return match ($state) { + TransactionState::COMMITTED => 'commit', + TransactionState::ACTIVE => 'begin', + TransactionState::ROLLED_BACK => 'rollback', + TransactionState::TERMINATED => 'error', + }; + } +} diff --git a/src/Factories/SymfonyDriverFactory.php b/src/Factories/SymfonyDriverFactory.php new file mode 100644 index 0000000..20d8168 --- /dev/null +++ b/src/Factories/SymfonyDriverFactory.php @@ -0,0 +1,97 @@ +generateTransactionId(); + + $handler = fn (): SymfonyTransaction => new SymfonyTransaction( + tsx: $session->beginTransaction(config: $config), + handler: $this->handler, + alias: $alias, + scheme: $schema, + transactionId: $tranactionId + ); + + return $this->handler->handleTransactionAction( + nextTransactionState: TransactionState::ACTIVE, + transactionId: $tranactionId, + runHandler: $handler, + alias: $alias, + scheme: $schema, + ); + } + + public function createSession( + Driver $driver, + ?SessionConfiguration $config, + string $alias, + string $schema, + ): SymfonySession { + return new SymfonySession( + session: $driver->createSession($config), + handler: $this->handler, + factory: $this, + alias: $alias, + schema: $schema, + ); + } + + public function createDriver( + Driver $driver, + string $alias, + string $schema, + ): SymfonyDriver { + return new SymfonyDriver( + $driver, + $this, + $alias, + $schema, + ); + } + + private function generateTransactionId(): string + { + if ($this->uuidFactory) { + return $this->uuidFactory->create()->toRfc4122(); + } + + $data = random_bytes(16); + + // Set the version to 4 (UUID v4) + $data[6] = chr((ord($data[6]) & 0x0F) | 0x40); + + // Set the variant to RFC 4122 (10xx) + $data[8] = chr((ord($data[8]) & 0x3F) | 0x80); + + // Format the UUID as 8-4-4-4-12 hexadecimal characters + return sprintf( + '%08s-%04s-%04s-%04s-%12s', + bin2hex(substr($data, 0, 4)), + bin2hex(substr($data, 4, 2)), + bin2hex(substr($data, 6, 2)), + bin2hex(substr($data, 8, 2)), + bin2hex(substr($data, 10, 6)) + ); + } +} diff --git a/src/SymfonyClient.php b/src/SymfonyClient.php deleted file mode 100644 index f1696fe..0000000 --- a/src/SymfonyClient.php +++ /dev/null @@ -1,136 +0,0 @@ -> - */ -class SymfonyClient implements ClientInterface -{ - /** - * @param ClientInterface> $client - */ - public function __construct( - private readonly ClientInterface $client, - private readonly EventHandler $handler, - ) { - } - - public function run(string $statement, iterable $parameters = [], ?string $alias = null): ?SummarizedResult - { - return $this->runStatement(new Statement($statement, $parameters), $alias); - } - - public function runStatement(Statement $statement, ?string $alias = null): ?SummarizedResult - { - return $this->handler->handle( - fn (Statement $statement) => $this->client->runStatement($statement, $alias), - $statement, - $alias, - null - ); - } - - public function runStatements(iterable $statements, ?string $alias = null): CypherList - { - $tbr = []; - foreach ($statements as $statement) { - $tbr[] = $this->runStatement($statement, $alias); - } - - return CypherList::fromIterable($tbr); - } - - public function beginTransaction( - ?iterable $statements = null, - ?string $alias = null, - ?TransactionConfiguration $config = null, - ): UnmanagedTransactionInterface { - $tsx = new SymfonyTransaction($this->client->beginTransaction(null, $alias, $config), $this->handler, $alias); - - $runHandler = fn (Statement $statement): CypherList => $tsx->runStatement($statement); - - foreach (($statements ?? []) as $statement) { - $this->handler->handle($runHandler, $statement, $alias, null); - } - - return $tsx; - } - - public function getDriver(?string $alias): DriverInterface - { - return $this->client->getDriver($alias); - } - - public function writeTransaction( - callable $tsxHandler, - ?string $alias = null, - ?TransactionConfiguration $config = null, - ) { - $sessionConfig = SessionConfiguration::default()->withAccessMode(AccessMode::READ()); - $session = $this->client->getDriver($alias)->createSession($sessionConfig); - - return TransactionHelper::retry( - fn () => new SymfonyTransaction($session->beginTransaction([], $config), $this->handler, $alias), - $tsxHandler - ); - } - - public function readTransaction( - callable $tsxHandler, - ?string $alias = null, - ?TransactionConfiguration $config = null, - ) { - $sessionConfig = SessionConfiguration::default()->withAccessMode(AccessMode::WRITE()); - $session = $this->client->getDriver($alias)->createSession($sessionConfig); - - return TransactionHelper::retry( - fn () => new SymfonyTransaction($session->beginTransaction([], $config), $this->handler, $alias), - $tsxHandler - ); - } - - public function transaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null) - { - return $this->writeTransaction($tsxHandler, $alias, $config); - } - - public function verifyConnectivity(?string $driver = null): bool - { - return $this->client->verifyConnectivity($driver); - } - - public function bindTransaction(?string $alias = null, ?TransactionConfiguration $config = null): void - { - $this->client->bindTransaction($alias, $config); - } - - public function commitBoundTransaction(?string $alias = null, int $depth = 1): void - { - $this->client->commitBoundTransaction($alias, $depth); - } - - public function rollbackBoundTransaction(?string $alias = null, int $depth = 1): void - { - $this->client->rollbackBoundTransaction($alias, $depth); - } - - public function hasDriver(string $alias): bool - { - return $this->client->hasDriver($alias); - } -} diff --git a/tests/App/Controller/TestController.php b/tests/App/Controller/TestController.php index 63397e5..226f00e 100644 --- a/tests/App/Controller/TestController.php +++ b/tests/App/Controller/TestController.php @@ -3,25 +3,52 @@ namespace Neo4j\Neo4jBundle\Tests\App\Controller; use Laudis\Neo4j\Contracts\ClientInterface; +use Laudis\Neo4j\Contracts\SessionInterface; +use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; class TestController extends AbstractController { - public function __construct( - private readonly ClientInterface $client, - ) { + public function __construct(private readonly LoggerInterface $logger) + { + } + + public function runOnClient(ClientInterface $client): Response + { + $client->run('MATCH (n {foo: $bar}) RETURN n', ['bar' => 'baz']); + try { + $client->run('MATCH (n) {x: $x}', ['x' => 1]); + } catch (\Exception) { + $this->logger->warning('Detected failed statement'); + } + + return $this->render('index.html.twig'); } - public function __invoke(): Response + public function runOnSession(SessionInterface $session): Response { - // Successful statement - $this->client->run('MATCH (n {foo: $bar}) RETURN n', ['bar' => 'baz']); + $session->run('MATCH (n {foo: $bar}) RETURN n', ['bar' => 'baz']); + try { + $session->run('MATCH (n) {x: $x}', ['x' => 1]); + } catch (\Exception) { + $this->logger->warning('Detected failed statement'); + } + + return $this->render('index.html.twig'); + } + + public function runOnTransaction(SessionInterface $session): Response + { + $tsx = $session->beginTransaction(); + + $tsx->run('MATCH (n {foo: $bar}) RETURN n', ['bar' => 'baz']); try { - // Failing statement - $this->client->run('MATCH (n) {x: $x}', ['x' => 1]); + $tsx->run('MATCH (n) {x: $x}', ['x' => 1]); } catch (\Exception) { - // ignore + $this->logger->warning('Detected failed statement'); + } finally { + $tsx->rollback(); } return $this->render('index.html.twig'); diff --git a/tests/App/config/routes.yaml b/tests/App/config/routes.yaml index 62a3882..ec58a78 100644 --- a/tests/App/config/routes.yaml +++ b/tests/App/config/routes.yaml @@ -1,6 +1,12 @@ -test: - path: / - controller: Neo4j\Neo4jBundle\Tests\App\Controller\TestController +run-on-client: + path: /client + controller: Neo4j\Neo4jBundle\Tests\App\Controller\TestController::runOnClient +run-on-session: + path: /session + controller: Neo4j\Neo4jBundle\Tests\App\Controller\TestController::runOnSession +run-on-transaction: + path: /transaction + controller: Neo4j\Neo4jBundle\Tests\App\Controller\TestController::runOnTransaction web_profiler_wdt: resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' diff --git a/tests/Functional/IntegrationTest.php b/tests/Functional/IntegrationTest.php index 9bd0f7c..9226402 100644 --- a/tests/Functional/IntegrationTest.php +++ b/tests/Functional/IntegrationTest.php @@ -6,7 +6,6 @@ namespace Neo4j\Neo4jBundle\Tests\Functional; -use Laudis\Neo4j\Client; use Laudis\Neo4j\Common\DriverSetupManager; use Laudis\Neo4j\Common\SingleThreadedSemaphore; use Laudis\Neo4j\Contracts\ClientInterface; @@ -17,11 +16,14 @@ use Laudis\Neo4j\Enum\SslMode; use Laudis\Neo4j\Neo4j\Neo4jConnectionPool; use Laudis\Neo4j\Neo4j\Neo4jDriver; -use Neo4j\Neo4jBundle\SymfonyClient; +use Neo4j\Neo4jBundle\Decorators\SymfonyClient; use Neo4j\Neo4jBundle\Tests\App\TestKernel; -use Psr\Http\Message\UriInterface; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +/** + * @psalm-suppress InternalMethod + * @psalm-suppress UndefinedInterfaceMethod + */ class IntegrationTest extends KernelTestCase { protected static function getKernelClass(): string @@ -78,9 +80,10 @@ public function testDefaultDsn(): void * @var Neo4jDriver $driver */ $driver = $client->getDriver('default'); - /** - * @var UriInterface $uri - */ + + $driver = $this->getPrivateProperty($driver, 'driver'); + $driver = $this->getPrivateProperty($driver, 'driver'); + $uri = $this->getPrivateProperty($driver, 'parsedUrl'); $this->assertSame($uri->getScheme(), 'neo4j'); @@ -114,6 +117,10 @@ public function testDriverAuthentication(): void $client = $container->get('neo4j.client'); /** @var Neo4jDriver $driver */ $driver = $client->getDriver('neo4j-auth'); + + $driver = $this->getPrivateProperty($driver, 'driver'); + $driver = $this->getPrivateProperty($driver, 'driver'); + /** @var Neo4jConnectionPool $pool */ $pool = $this->getPrivateProperty($driver, 'pool'); /** @var ConnectionRequestData $data */ @@ -139,6 +146,10 @@ public function testDefaultDriverConfig(): void $client = $container->get('neo4j.client'); /** @var Neo4jDriver $driver */ $driver = $client->getDriver('default'); + + $driver = $this->getPrivateProperty($driver, 'driver'); + $driver = $this->getPrivateProperty($driver, 'driver'); + /** @var Neo4jConnectionPool $pool */ $pool = $this->getPrivateProperty($driver, 'pool'); /** @var SingleThreadedSemaphore $semaphore */ @@ -174,9 +185,7 @@ public function testDefaultSessionConfig(): void * @var ClientInterface $client */ $client = $container->get('neo4j.client'); - /** @var Client $innerClient */ - $innerClient = $this->getPrivateProperty($client, 'client'); - $sessionConfig = $innerClient->getDefaultSessionConfiguration(); + $sessionConfig = $client->getDefaultSessionConfiguration(); $this->assertSame($sessionConfig->getFetchSize(), 999); } @@ -190,9 +199,7 @@ public function testDefaultTransactionConfig(): void * @var ClientInterface $client */ $client = $container->get('neo4j.client'); - /** @var Client $innerClient */ - $innerClient = $this->getPrivateProperty($client, 'client'); - $transactionConfig = $innerClient->getDefaultTransactionConfiguration(); + $transactionConfig = $client->getDefaultTransactionConfiguration(); $this->assertSame($transactionConfig->getTimeout(), 40.0); } @@ -218,10 +225,8 @@ public function testPriority(): void * @var ClientInterface $client */ $client = $container->get('neo4j.client'); - /** @var Client $innerClient */ - $innerClient = $this->getPrivateProperty($client, 'client'); /** @var DriverSetupManager $drivers */ - $drivers = $this->getPrivateProperty($innerClient, 'driverSetups'); + $drivers = $this->getPrivateProperty($client, 'driverSetups'); /** @var array<\SplPriorityQueue> $fallbackDriverQueue */ $driverSetups = $this->getPrivateProperty($drivers, 'driverSetups'); /** @var \SplPriorityQueue $fallbackDriverQueue */ @@ -244,20 +249,17 @@ public function testDefaultLogLevel(): void $client = $container->get('neo4j.client'); /** @var Neo4jDriver $driver */ $driver = $client->getDriver('default'); + + $driver = $this->getPrivateProperty($driver, 'driver'); + $driver = $this->getPrivateProperty($driver, 'driver'); + /** @var Neo4jConnectionPool $pool */ $pool = $this->getPrivateProperty($driver, 'pool'); - $level = $pool->getLogger()->getLevel(); + $level = $pool->getLogger()?->getLevel(); $this->assertSame('warning', $level); } - /** - * @template T - * - * @return T - * - * @noinspection PhpDocMissingThrowsInspection - */ private function getPrivateProperty(object $object, string $property): mixed { $reflection = new \ReflectionClass($object); diff --git a/tests/Functional/ProfilerTest.php b/tests/Functional/ProfilerTest.php index 623b079..cf2f48f 100644 --- a/tests/Functional/ProfilerTest.php +++ b/tests/Functional/ProfilerTest.php @@ -18,8 +18,49 @@ public function testProfiler(): void $client = static::createClient(); $client->enableProfiler(); - // Calls Neo4j\Neo4jBundle\Tests\App\Controller\TestController::__invoke - $client->request('GET', '/'); + $client->request('GET', '/client'); + + if ($profile = $client->getProfile()) { + /** @var Neo4jDataCollector $collector */ + $collector = $profile->getCollector('neo4j'); + $this->assertEquals( + 2, + $collector->getQueryCount() + ); + $successfulStatements = $collector->getSuccessfulStatements(); + $failedStatements = $collector->getFailedStatements(); + $this->assertCount(1, $successfulStatements); + $this->assertCount(1, $failedStatements); + } + } + + public function testProfilerOnSession(): void + { + $client = static::createClient(); + $client->enableProfiler(); + + $client->request('GET', '/session'); + + if ($profile = $client->getProfile()) { + /** @var Neo4jDataCollector $collector */ + $collector = $profile->getCollector('neo4j'); + $this->assertEquals( + 2, + $collector->getQueryCount() + ); + $successfulStatements = $collector->getSuccessfulStatements(); + $failedStatements = $collector->getFailedStatements(); + $this->assertCount(1, $successfulStatements); + $this->assertCount(1, $failedStatements); + } + } + + public function testProfilerOnTransaction(): void + { + $client = static::createClient(); + $client->enableProfiler(); + + $client->request('GET', '/transaction'); if ($profile = $client->getProfile()) { /** @var Neo4jDataCollector $collector */