diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index ca1cfde..d0c7cc7 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -4,8 +4,6 @@ on: branches: - master pull_request: - branches: - - master jobs: php-cs-fixer: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1aa931e..3058801 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,8 +4,6 @@ on: branches: - master pull_request: - branches: - - master jobs: build: diff --git a/Dockerfile b/Dockerfile index 9596b04..d3f46b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,9 @@ RUN apt-get update \ && docker-php-ext-enable xdebug \ && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer -WORKDIR /opt/project - +RUN echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini +RUN echo "xdebug.mode=debug,develop" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini +WORKDIR /opt/project +CMD ["php", "-S", "0.0.0.0:80", "-t", "/opt/project/tests/App"] diff --git a/bin/console.php b/bin/console.php index 645f886..6abacd8 100644 --- a/bin/console.php +++ b/bin/console.php @@ -5,6 +5,6 @@ require __DIR__ . '/../vendor/autoload.php'; -$console = new Application(new TestKernel('test', true)); +$console = new Application(new TestKernel($_ENV['APP_ENV'] ?? 'dev', true)); $console->run(); \ No newline at end of file diff --git a/composer.json b/composer.json index 63870d2..1bf4ae2 100644 --- a/composer.json +++ b/composer.json @@ -19,18 +19,22 @@ "symfony/config": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { + "friendsofphp/php-cs-fixer": "^3.30", + "kubawerlos/php-cs-fixer-custom-fixers": "^3.0", "matthiasnoback/symfony-dependency-injection-test": "^4.3 || ^5.0", "phpunit/phpunit": "^9.5", + "psalm/plugin-phpunit": "^0.18", "psalm/plugin-symfony": "^5.0", "symfony/console": "^5.4 || ^6.0 || ^7.0", "symfony/framework-bundle": "^5.4 || ^6.0 || ^7.0", "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/routing": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^6.4", "symfony/test-pack": "^1.1", + "symfony/twig-bundle": "^5.4 || ^6.0 || ^7.0", + "symfony/web-profiler-bundle": "^5.4 || ^6.0 || ^7.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0", - "vimeo/psalm": "^5.15.0", - "kubawerlos/php-cs-fixer-custom-fixers": "^3.0", - "friendsofphp/php-cs-fixer": "^3.30", - "psalm/plugin-phpunit": "^0.18" + "vimeo/psalm": "^5.15.0" }, "autoload": { "psr-4": { @@ -49,7 +53,7 @@ } }, "scripts": { - "psalm": "php bin/console.php cache:warmup && vendor/bin/psalm --show-info=true", + "psalm": "APP_ENV=dev php bin/console.php cache:warmup && vendor/bin/psalm --show-info=true", "fix-cs": "vendor/bin/php-cs-fixer fix", "check-cs": "vendor/bin/php-cs-fixer fix --dry-run", "ci-symfony-install-version": "./.github/scripts/setup-symfony-env.bash" diff --git a/config/services.php b/config/services.php index 957a43f..59dd13f 100644 --- a/config/services.php +++ b/config/services.php @@ -7,7 +7,7 @@ use Laudis\Neo4j\Contracts\SessionInterface; use Laudis\Neo4j\Contracts\TransactionInterface; use Neo4j\Neo4jBundle\ClientFactory; -use Neo4j\Neo4jBundle\EventHandler; +use Neo4j\Neo4jBundle\EventListener\Neo4jProfileListener; use Neo4j\Neo4jBundle\SymfonyClient; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -16,10 +16,6 @@ return static function (ContainerConfigurator $configurator) { $services = $configurator->services(); - $services->set('neo4j.event_handler', EventHandler::class) - ->autowire() - ->autoconfigure(); - $services->set('neo4j.client_factory', ClientFactory::class) ->args([ service('neo4j.event_handler'), @@ -47,4 +43,7 @@ $services->alias(DriverInterface::class, 'neo4j.driver'); $services->alias(SessionInterface::class, 'neo4j.session'); $services->alias(TransactionInterface::class, 'neo4j.transaction'); + + $services->set('neo4j.subscriber', Neo4jProfileListener::class) + ->tag('kernel.event_subscriber'); }; diff --git a/docker-compose.yml b/docker-compose.yml index 8e5b808..c28d0d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,10 @@ services: - NEO4J_PORT=7687 - NEO4J_USER=neo4j - NEO4J_PASSWORD=testtest + - XDEBUG_CONFIG="client_host=host.docker.internal log=/tmp/xdebug.log" working_dir: /opt/project + extra_hosts: + - "host.docker.internal:host-gateway" networks: - neo4j-symfony diff --git a/psalm.xml b/psalm.xml index ce09bf1..a81eac8 100644 --- a/psalm.xml +++ b/psalm.xml @@ -18,7 +18,7 @@ - var/cache/test/Neo4j_Neo4jBundle_Tests_App_TestKernelTestDebugContainer.xml + var/cache/dev/Neo4j_Neo4jBundle_Tests_App_TestKernelDevDebugContainer.xml diff --git a/src/ClientFactory.php b/src/ClientFactory.php index 972f7e1..7d8d419 100644 --- a/src/ClientFactory.php +++ b/src/ClientFactory.php @@ -56,15 +56,15 @@ public function create(): SymfonyClient /** @var ClientBuilder> $builder */ $builder = ClientBuilder::create(); - if ($this->driverConfig) { + if (null !== $this->driverConfig) { $builder = $builder->withDefaultDriverConfiguration($this->makeDriverConfig()); } - if ($this->sessionConfiguration) { + if (null !== $this->sessionConfiguration) { $builder = $builder->withDefaultSessionConfiguration($this->makeSessionConfig()); } - if ($this->transactionConfiguration) { + if (null !== $this->transactionConfiguration) { $builder = $builder->withDefaultTransactionConfiguration($this->makeTransactionConfig()); } @@ -77,7 +77,7 @@ public function create(): SymfonyClient ); } - if ($this->defaultDriver) { + if (null !== $this->defaultDriver) { $builder = $builder->withDefaultDriver($this->defaultDriver); } diff --git a/src/Collector/Neo4jDataCollector.php b/src/Collector/Neo4jDataCollector.php new file mode 100644 index 0000000..4099a5c --- /dev/null +++ b/src/Collector/Neo4jDataCollector.php @@ -0,0 +1,148 @@ +> | list, + * } $data + */ +final class Neo4jDataCollector extends AbstractDataCollector +{ + public function __construct( + private readonly Neo4jProfileListener $subscriber, + ) { + } + + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void + { + $t = $this; + $profiledSummaries = $this->subscriber->getProfiledSummaries(); + $successfulStatements = []; + foreach ($profiledSummaries as $summary) { + $statement = ['status' => 'success']; + foreach ($summary as $key => $value) { + if (!is_array($value) && !is_object($value)) { + $statement[$key] = $value; + continue; + } + + $statement[$key] = $t->recursiveToArray($value); + } + $successfulStatements[] = $statement; + } + + $failedStatements = array_map( + static fn (array $x) => [ + 'status' => 'failure', + 'time' => $x['time'], + 'timestamp' => $x['timestamp'], + 'result' => [ + 'statement' => $x['statement']->toArray(), + ], + 'exception' => [ + 'code' => $x['exception']->getErrors()[0]->getCode(), + 'message' => $x['exception']->getErrors()[0]->getMessage(), + 'classification' => $x['exception']->getErrors()[0]->getClassification(), + 'category' => $x['exception']->getErrors()[0]->getCategory(), + 'title' => $x['exception']->getErrors()[0]->getTitle(), + ], + 'alias' => $x['alias'], + ], + $this->subscriber->getProfiledFailures() + ); + + $this->data['successful_statements_count'] = count($successfulStatements); + $this->data['failed_statements_count'] = count($failedStatements); + $mergedArray = array_merge($successfulStatements, $failedStatements); + uasort( + $mergedArray, + static fn (array $a, array $b) => $a['start_time'] <=> $b['timestamp'] + ); + $this->data['statements'] = $mergedArray; + } + + public function reset(): void + { + parent::reset(); + $this->subscriber->reset(); + } + + public function getName(): string + { + return 'neo4j'; + } + + /** @api */ + public function getStatements(): array + { + return $this->data['statements']; + } + + public function getSuccessfulStatements(): array + { + return array_filter( + $this->data['statements'], + static fn (array $x) => 'success' === $x['status'] + ); + } + + public function getFailedStatements(): array + { + return array_filter( + $this->data['statements'], + static fn (array $x) => 'failure' === $x['status'] + ); + } + + /** @api */ + public function getFailedStatementsCount(): array + { + return $this->data['failed_statements_count']; + } + + /** @api */ + public function getSuccessfulStatementsCount(): array + { + return $this->data['successful_statements_count']; + } + + public function getQueryCount(): int + { + return count($this->data['statements']); + } + + public static function getTemplate(): ?string + { + return '@Neo4j/web_profiler.html.twig'; + } + + private function recursiveToArray(mixed $obj): mixed + { + if (is_array($obj)) { + return array_map( + fn (mixed $x): mixed => $this->recursiveToArray($x), + $obj + ); + } + + if (is_object($obj) && method_exists($obj, 'toArray')) { + return $obj->toArray(); + } + + return $obj; + } +} diff --git a/src/DependencyInjection/Neo4jExtension.php b/src/DependencyInjection/Neo4jExtension.php index bf491ec..d366309 100644 --- a/src/DependencyInjection/Neo4jExtension.php +++ b/src/DependencyInjection/Neo4jExtension.php @@ -4,12 +4,16 @@ namespace Neo4j\Neo4jBundle\DependencyInjection; +use Neo4j\Neo4jBundle\Collector\Neo4jDataCollector; +use Neo4j\Neo4jBundle\EventHandler; +use Neo4j\Neo4jBundle\EventListener\Neo4jProfileListener; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; use Symfony\Component\DependencyInjection\Reference; @@ -27,6 +31,13 @@ public function load(array $configs, ContainerBuilder $container): ContainerBuil $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config')); $loader->load('services.php'); + $defaultAlias = $mergedConfig['default_driver'] ?? $mergedConfig['drivers'][0]['alias'] ?? 'default'; + + $container->setDefinition('neo4j.event_handler', new Definition(EventHandler::class)) + ->setAutowired(true) + ->addTag('neo4j.event_handler') + ->setArgument(1, $defaultAlias); + $container->getDefinition('neo4j.client_factory') ->setArgument(1, $mergedConfig['default_driver_config'] ?? null) ->setArgument(2, $mergedConfig['default_session_config'] ?? null) @@ -34,21 +45,49 @@ public function load(array $configs, ContainerBuilder $container): ContainerBuil ->setArgument(4, $mergedConfig['drivers'] ?? []) ->setArgument(5, $mergedConfig['default_driver'] ?? null) ->setArgument(6, new Reference(ClientInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) - ->setArgument(7, new Reference(StreamFactoryInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) - ->setArgument(8, new Reference(RequestFactoryInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE)) - ->setAbstract(false) - ; + ->setArgument( + 7, + new Reference(StreamFactoryInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE) + ) + ->setArgument( + 8, + new Reference(RequestFactoryInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE) + ) + ->setAbstract(false); $container->getDefinition('neo4j.driver') - ->setArgument(0, $mergedConfig['drivers']['alias'] ?? 'default'); + ->setArgument(0, $defaultAlias); $enabledProfiles = []; foreach ($mergedConfig['drivers'] as $driver) { - if (true === $driver['profiling'] || (null === $driver['profiling'] && $container->getParameter('kernel.debug'))) { + if (true === $driver['profiling'] || (null === $driver['profiling'] && $container->getParameter( + 'kernel.debug' + ))) { $enabledProfiles[] = $driver['alias']; } } + if (0 !== count($enabledProfiles)) { + $container->setDefinition( + 'neo4j.data_collector', + (new Definition(Neo4jDataCollector::class)) + ->setAutowired(true) + ->addTag('data_collector', [ + 'id' => 'neo4j', + 'priority' => 500, + ]) + ); + + $container->setAlias(Neo4jProfileListener::class, 'neo4j.subscriber'); + + $container->setDefinition( + 'neo4j.subscriber', + (new Definition(Neo4jProfileListener::class)) + ->setArgument(0, $enabledProfiles) + ->addTag('kernel.event_subscriber') + ); + } + return $container; } diff --git a/src/Event/FailureEvent.php b/src/Event/FailureEvent.php index 2981193..50ab49f 100644 --- a/src/Event/FailureEvent.php +++ b/src/Event/FailureEvent.php @@ -14,8 +14,13 @@ class FailureEvent extends Event protected bool $shouldThrowException = true; - public function __construct(private ?string $alias, private Statement $statement, private Neo4jException $exception) - { + public function __construct( + private readonly ?string $alias, + private readonly Statement $statement, + private readonly Neo4jException $exception, + private readonly \DateTimeInterface $time, + private readonly ?string $scheme, + ) { } public function getException(): Neo4jException @@ -23,6 +28,7 @@ public function getException(): Neo4jException return $this->exception; } + /** @api */ public function disableException(): void { $this->shouldThrowException = false; @@ -33,6 +39,11 @@ public function shouldThrowException(): bool return $this->shouldThrowException; } + public function getTime(): \DateTimeInterface + { + return $this->time; + } + public function getAlias(): ?string { return $this->alias; @@ -42,4 +53,9 @@ public function getStatement(): Statement { return $this->statement; } + + public function getScheme(): ?string + { + return $this->scheme; + } } diff --git a/src/Event/PostRunEvent.php b/src/Event/PostRunEvent.php index b0bf84b..38c38da 100644 --- a/src/Event/PostRunEvent.php +++ b/src/Event/PostRunEvent.php @@ -12,8 +12,10 @@ class PostRunEvent extends Event public const EVENT_ID = 'neo4j.post_run'; public function __construct( - private ?string $alias, - private ResultSummary $result + private readonly ?string $alias, + private readonly ResultSummary $result, + private readonly \DateTimeInterface $time, + private readonly ?string $scheme, ) { } @@ -22,8 +24,18 @@ public function getResult(): ResultSummary return $this->result; } + public function getTime(): \DateTimeInterface + { + return $this->time; + } + public function getAlias(): ?string { return $this->alias; } + + public function getScheme(): ?string + { + return $this->scheme; + } } diff --git a/src/Event/PreRunEvent.php b/src/Event/PreRunEvent.php index 6ab3f4d..8772a15 100644 --- a/src/Event/PreRunEvent.php +++ b/src/Event/PreRunEvent.php @@ -11,17 +11,32 @@ class PreRunEvent extends Event { public const EVENT_ID = 'neo4j.pre_run'; - public function __construct(private ?string $alias, private Statement $statement) - { + public function __construct( + private readonly ?string $alias, + private readonly Statement $statement, + private readonly \DateTimeInterface $time, + private readonly ?string $scheme, + ) { } + /** @api */ public function getStatement(): Statement { return $this->statement; } + public function getTime(): \DateTimeInterface + { + return $this->time; + } + public function getAlias(): ?string { return $this->alias; } + + public function getScheme(): ?string + { + return $this->scheme; + } } diff --git a/src/EventHandler.php b/src/EventHandler.php index ce2a83c..0f44d1f 100644 --- a/src/EventHandler.php +++ b/src/EventHandler.php @@ -18,14 +18,18 @@ use Neo4j\Neo4jBundle\Event\FailureEvent; use Neo4j\Neo4jBundle\Event\PostRunEvent; use Neo4j\Neo4jBundle\Event\PreRunEvent; +use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; class EventHandler { private ?EventDispatcherInterface $dispatcher; - public function __construct(?EventDispatcherInterface $dispatcher) - { + public function __construct( + ?EventDispatcherInterface $dispatcher, + private readonly string $alias, + private readonly ?Stopwatch $stopwatch, + ) { $this->dispatcher = $dispatcher; } @@ -36,19 +40,34 @@ public function __construct(?EventDispatcherInterface $dispatcher) * * @return SummarizedResult */ - public function handle(callable $runHandler, Statement $statement, ?string $alias): SummarizedResult + public function handle(callable $runHandler, Statement $statement, ?string $alias, ?string $scheme): SummarizedResult { + $stopWatchName = sprintf('neo4j.%s.query', $alias ?? $this->alias); if (null === $this->dispatcher) { - return $runHandler($statement); + $this->stopwatch?->start($stopWatchName); + $result = $runHandler($statement); + $this->stopwatch?->stop($stopWatchName); + + return $result; } - $this->dispatcher->dispatch(new PreRunEvent($alias, $statement), PreRunEvent::EVENT_ID); + /** @noinspection PhpUnhandledExceptionInspection */ + $time = new \DateTimeImmutable('now', new \DateTimeZone(date_default_timezone_get())); + $this->dispatcher->dispatch(new PreRunEvent($alias, $statement, $time, $scheme), PreRunEvent::EVENT_ID); try { + $this->stopwatch?->start($stopWatchName); $tbr = $runHandler($statement); - $this->dispatcher->dispatch(new PostRunEvent($alias, $tbr->getSummary()), PostRunEvent::EVENT_ID); + $this->stopwatch?->stop($stopWatchName); + $this->dispatcher->dispatch( + new PostRunEvent($alias ?? $this->alias, $tbr->getSummary(), $time, $scheme), + PostRunEvent::EVENT_ID + ); } catch (Neo4jException $e) { - $event = new FailureEvent($alias, $statement, $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()) { diff --git a/src/EventListener/Neo4jProfileListener.php b/src/EventListener/Neo4jProfileListener.php new file mode 100644 index 0000000..e5c91d7 --- /dev/null +++ b/src/EventListener/Neo4jProfileListener.php @@ -0,0 +1,108 @@ + + */ + private array $profiledSummaries = []; + + /** + * @var list + */ + private array $profiledFailures = []; + + /** + * @param list $enabledProfiles + */ + public function __construct(private readonly array $enabledProfiles = []) + { + } + + public static function getSubscribedEvents(): array + { + return [ + PostRunEvent::EVENT_ID => 'onPostRun', + FailureEvent::EVENT_ID => 'onFailure', + ]; + } + + public function onPostRun(PostRunEvent $event): void + { + if (in_array($event->getAlias(), $this->enabledProfiles)) { + $time = $event->getTime(); + $result = $event->getResult(); + $end_time = $time->getTimestamp() + $result->getResultAvailableAfter() + $result->getResultConsumedAfter(); + $this->profiledSummaries[] = [ + 'result' => $event->getResult(), + 'alias' => $event->getAlias(), + 'time' => $time->format('Y-m-d H:i:s'), + 'start_time' => $time->getTimestamp(), + 'end_time' => $end_time, + ]; + } + } + + public function onFailure(FailureEvent $event): void + { + if (in_array($event->getAlias(), $this->enabledProfiles)) { + $time = $event->getTime(); + $this->profiledFailures[] = [ + 'exception' => $event->getException(), + 'statement' => $event->getStatement(), + 'alias' => $event->getAlias(), + 'time' => $time->format('Y-m-d H:i:s'), + 'timestamp' => $time->getTimestamp(), + ]; + } + } + + public function getProfiledSummaries(): array + { + return $this->profiledSummaries; + } + + /** + * @return list + */ + public function getProfiledFailures(): array + { + return $this->profiledFailures; + } + + public function reset(): void + { + $this->profiledFailures = []; + $this->profiledSummaries = []; + } +} diff --git a/templates/web_profiler.html.twig b/src/Resources/views/web_profiler.html.twig similarity index 55% rename from templates/web_profiler.html.twig rename to src/Resources/views/web_profiler.html.twig index 8393225..35cec19 100644 --- a/templates/web_profiler.html.twig +++ b/src/Resources/views/web_profiler.html.twig @@ -4,7 +4,7 @@ {% block toolbar %} {% if collector.queryCount > 0 %} {% set icon %} - {{ include('@Neo4j/neo4j.svg') }} +{# {{ include('@Neo4j/public/images/neo4j.svg') }}#} {{ collector.queryCount }} stmt. {% endset %} @@ -22,7 +22,7 @@ {% endif %}
Total time - {{ '%0.2f'|format(collector.time) }}ms +{# {{ '%0.2f'|format(collector.time) }}ms#}
{% endset %} @@ -30,17 +30,17 @@ {% endif %} {% endblock %} -{% block head %} - - - {{ parent() }} -{% endblock %} +{#{% block head %}#} +{# #} +{# #} +{# {{ parent() }}#} +{#{% endblock %}#} {% block menu %} {# This left-hand menu appears when using the full-screen profiler. #} - {{ include('@Neo4j/Icon/neo4j.svg') }} +{# {{ include('@Neo4j/Icon/neo4j.svg') }}#} Neo4j {% if collector.failedStatements|length %} @@ -60,19 +60,25 @@ # Status Query + Query Time + Query Type + Database + Executed At + Exception Title - {% for idx, statement in collector.successfulStatements %} + {% for idx, statement in collector.statements %} + {% set status = statement.status|default('unknown') %} {% set start_time = statement.start_time|default(null) %} {% set end_time = statement.end_time|default(null) %} - {{ idx + 1 }} - {% if start_time is not null and end_time is not null %}{{ '%0.2f'|format(end_time - start_time) }}ms{% endif %} + {{ idx + 1 }} + {{ statement.status }}
- {{ statement.query|default('') }} + {{ statement.result.statement.text|default('') }}
View details @@ -80,31 +86,34 @@
- Parameters: {{ statement.parameters|default([])|yaml_encode }} -
-
- Tag: {{ statement.tag|default('N/A')|yaml_encode }} + Parameters: {{ statement.result.statement.parameters|default([])|json_encode }}
- {% if statement.success %}
- Number of results: {{ statement.nb_results }} + Alias: {{ statement.alias|default('N/A') }}
+ {# TODO: Add scheme. Scheme is currently private in the underlying driver #} +{#
#} +{# Scheme: {{ statement.scheme|default('N/A') }}#} +{#
#}
- Scheme: {{ statement.scheme }} + Statistics: {{ statement|json_encode }}
-
- Statistics: {{ statement.statistics|yaml_encode }} -
- {% else %} -
- Type: {{ statement.exceptionCode }} -
-
- message: {{ statement.exceptionMessage }} -
- {% endif %}
+ {% if status is same as('success') %}{% if start_time is not null and end_time is not null %}{{ '%0.2f'|format(end_time - start_time) }}ms{% endif %}{% else %}N/A{% endif %} + {% if status is same as('success') %} + {{ statement.result.queryType }} + {{ statement.result.databaseInfo.name }} + {% else %} + N/A + N/A + {% endif %} + {{ statement.time }} + {% if status is same as('success') %} + N/A + {% else %} + {{ statement.exception.title }} + {% endif %} {% endfor %} diff --git a/src/SymfonyClient.php b/src/SymfonyClient.php index 615f748..f1696fe 100644 --- a/src/SymfonyClient.php +++ b/src/SymfonyClient.php @@ -25,8 +25,8 @@ class SymfonyClient implements ClientInterface * @param ClientInterface> $client */ public function __construct( - private ClientInterface $client, - private EventHandler $handler + private readonly ClientInterface $client, + private readonly EventHandler $handler, ) { } @@ -37,7 +37,12 @@ public function run(string $statement, iterable $parameters = [], ?string $alias public function runStatement(Statement $statement, ?string $alias = null): ?SummarizedResult { - return $this->handler->handle(fn (Statement $statement) => $this->client->runStatement($statement, $alias), $statement, $alias); + return $this->handler->handle( + fn (Statement $statement) => $this->client->runStatement($statement, $alias), + $statement, + $alias, + null + ); } public function runStatements(iterable $statements, ?string $alias = null): CypherList @@ -50,14 +55,17 @@ public function runStatements(iterable $statements, ?string $alias = null): Cyph return CypherList::fromIterable($tbr); } - public function beginTransaction(?iterable $statements = null, ?string $alias = null, ?TransactionConfiguration $config = null): UnmanagedTransactionInterface - { + 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); + $this->handler->handle($runHandler, $statement, $alias, null); } return $tsx; @@ -68,8 +76,11 @@ public function getDriver(?string $alias): DriverInterface return $this->client->getDriver($alias); } - public function writeTransaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null) - { + public function writeTransaction( + callable $tsxHandler, + ?string $alias = null, + ?TransactionConfiguration $config = null, + ) { $sessionConfig = SessionConfiguration::default()->withAccessMode(AccessMode::READ()); $session = $this->client->getDriver($alias)->createSession($sessionConfig); @@ -79,8 +90,11 @@ public function writeTransaction(callable $tsxHandler, ?string $alias = null, ?T ); } - public function readTransaction(callable $tsxHandler, ?string $alias = null, ?TransactionConfiguration $config = null) - { + public function readTransaction( + callable $tsxHandler, + ?string $alias = null, + ?TransactionConfiguration $config = null, + ) { $sessionConfig = SessionConfiguration::default()->withAccessMode(AccessMode::WRITE()); $session = $this->client->getDriver($alias)->createSession($sessionConfig); diff --git a/src/SymfonyTransaction.php b/src/SymfonyTransaction.php index bc8a46a..603b732 100644 --- a/src/SymfonyTransaction.php +++ b/src/SymfonyTransaction.php @@ -18,8 +18,11 @@ class SymfonyTransaction implements UnmanagedTransactionInterface /** * @param UnmanagedTransactionInterface> $tsx */ - public function __construct(private UnmanagedTransactionInterface $tsx, private EventHandler $handler, private ?string $alias) - { + public function __construct( + private readonly UnmanagedTransactionInterface $tsx, + private readonly EventHandler $handler, + private readonly ?string $alias, + ) { } public function run(string $statement, iterable $parameters = []): SummarizedResult @@ -29,7 +32,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), $statement, $this->alias); + return $this->handler->handle(fn ($statement) => $this->tsx->runStatement($statement), + $statement, + $this->alias, + null + ); } /** diff --git a/tests/App/Controller/TestController.php b/tests/App/Controller/TestController.php new file mode 100644 index 0000000..98969b0 --- /dev/null +++ b/tests/App/Controller/TestController.php @@ -0,0 +1,31 @@ +client->run('MATCH (n {foo: $bar}) RETURN n', ['bar' => 'baz']); + try { + // Failing statement + $this->client->run('MATCH (n) {x: $x}', ['x' => 1]); + } catch (\Exception) { + // ignore + } + + return $this->render('index.html.twig'); + } +} diff --git a/tests/App/Controller/Twig/base.html.twig b/tests/App/Controller/Twig/base.html.twig new file mode 100644 index 0000000..7d88c9e --- /dev/null +++ b/tests/App/Controller/Twig/base.html.twig @@ -0,0 +1,27 @@ + + + + + {% block title %}My Application{% endblock %} + {% block stylesheets %} + {# Include your stylesheets here #} + {% endblock %} + + +
+ {# Include your navigation/header here #} +
+ +
+ {% block body %}{% endblock %} +
+ +
+ {# Include your footer here #} +
+ +{% block javascripts %} +{# Include your JavaScripts here #} +{% endblock %} + + \ No newline at end of file diff --git a/tests/App/Controller/Twig/index.html.twig b/tests/App/Controller/Twig/index.html.twig new file mode 100644 index 0000000..fa75d44 --- /dev/null +++ b/tests/App/Controller/Twig/index.html.twig @@ -0,0 +1,7 @@ +{% extends 'base.html.twig' %} + +{% block title %}Hello Neo4j{% endblock %} + +{% block body %} +

Hello Neo4j

+{% endblock %} \ No newline at end of file diff --git a/tests/App/TestKernel.php b/tests/App/TestKernel.php index d8192cd..7512766 100644 --- a/tests/App/TestKernel.php +++ b/tests/App/TestKernel.php @@ -6,6 +6,8 @@ use Neo4j\Neo4jBundle\Neo4jBundle; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\TwigBundle\TwigBundle; +use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\HttpKernel\Kernel; @@ -15,6 +17,8 @@ public function registerBundles(): array { return [ new FrameworkBundle(), + new TwigBundle(), + new WebProfilerBundle(), new Neo4jBundle(), ]; } diff --git a/tests/App/config/default.yml b/tests/App/config/default.yml index 323ae51..4b7102d 100644 --- a/tests/App/config/default.yml +++ b/tests/App/config/default.yml @@ -1,6 +1,29 @@ + +services: + Neo4j\Neo4jBundle\Tests\App\Controller\TestController: + public: true + autoconfigure: true + autowire: true + tags: ['controller.service_arguments'] + Symfony\Component\HttpKernel\Profiler\Profiler: '@profiler' + Symfony\Component\HttpKernel\EventListener\ProfilerListener: '@profiler_listener' + framework: secret: test test: true + profiler: { enabled: true, collect: true } + router: + resource: '%kernel.project_dir%/tests/App/config/routes.yaml' + type: 'yaml' + +twig: + debug: "%kernel.debug%" + paths: + - '%kernel.project_dir%/tests/App/Controller/Twig' + +web_profiler: + toolbar: true + intercept_redirects: false parameters: neo4j.dsn.badname: bolt://localhost diff --git a/tests/App/config/routes.yaml b/tests/App/config/routes.yaml new file mode 100644 index 0000000..62a3882 --- /dev/null +++ b/tests/App/config/routes.yaml @@ -0,0 +1,10 @@ +test: + path: / + controller: Neo4j\Neo4jBundle\Tests\App\Controller\TestController + +web_profiler_wdt: + resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' + prefix: /_wdt +web_profiler_profiler: + resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' + prefix: /_profiler4 \ No newline at end of file diff --git a/tests/App/index.php b/tests/App/index.php new file mode 100644 index 0000000..3f89bde --- /dev/null +++ b/tests/App/index.php @@ -0,0 +1,28 @@ +boot(); +$request = Request::createFromGlobals(); +$response = $kernel->handle($request); +if ($kernel->getContainer()->has('profiler')) { + /** @var Symfony\Component\HttpKernel\Profiler\Profiler $profiler */ + $profiler = $kernel->getContainer()->get('profiler'); + $profile = $profiler->collect($request, $response); + if (null === $profile) { + error_log('Profiler token was not generated!!!'); + } else { + error_log('Profiler token: '.$profile->getToken()); + } +} else { + error_log('Profiler service not found in container'); +} +$response->send(); +$kernel->terminate($request, $response); diff --git a/tests/Functional/ProfilerTest.php b/tests/Functional/ProfilerTest.php new file mode 100644 index 0000000..623b079 --- /dev/null +++ b/tests/Functional/ProfilerTest.php @@ -0,0 +1,37 @@ +enableProfiler(); + + // Calls Neo4j\Neo4jBundle\Tests\App\Controller\TestController::__invoke + $client->request('GET', '/'); + + 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); + } + } +}