diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml
index 7c1936b..dcff7f6 100644
--- a/.github/workflows/static-analysis.yml
+++ b/.github/workflows/static-analysis.yml
@@ -19,6 +19,6 @@ jobs:
php_version: 8.0
version: 2
- name: "PHP-CS-Fixer"
- run: vendor/bin/php-cs-fixer fix --dry-run
+ run: composer check-cs
- name: "PSalm"
- run: ./console debug:config && vendor/bin/psalm --show-info=true
+ run: composer psalm
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 2a0eb5e..f75f202 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -15,14 +15,8 @@ jobs:
max-parallel: 10
matrix:
php: [ '8.0', '8.1', '8.2']
- sf_version: [ '5.4.*', '6.2.*', '6.3.*' ]
+ sf_version: [ '5.4.*', '6.0.*', '6.3.*' ]
exclude:
- - php: 7.4
- sf_version: 6.2.*
- - php: 8.0
- sf_version: 6.2.*
- - php: 7.4
- sf_version: 6.3.*
- php: 8.0
sf_version: 6.3.*
@@ -39,5 +33,4 @@ jobs:
php_version: ${{ matrix.php }}
memory_limit: 1024M
version: 9
- testsuite: Unit
bootstrap: vendor/autoload.php
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
index fe1cbef..65763ce 100644
--- a/.php-cs-fixer.dist.php
+++ b/.php-cs-fixer.dist.php
@@ -24,6 +24,7 @@
try {
$finder = PhpCsFixer\Finder::create()
->in(__DIR__.'/src')
+ ->in(__DIR__.'/config')
->in(__DIR__.'/tests');
} catch (Throwable $e) {
echo $e->getMessage()."\n";
@@ -35,28 +36,6 @@
->setRiskyAllowed(true)
->setRules([
'@Symfony' => true,
-
- 'array_syntax' => ['syntax' => 'short'],
- 'linebreak_after_opening_tag' => true,
- 'ordered_imports' => true,
- 'phpdoc_order' => true,
- 'phpdoc_to_comment' => false,
- 'yoda_style' => false,
- 'declare_strict_types' => true,
- 'global_namespace_import' => [
- 'import_classes' => true,
- 'import_constants' => true,
- 'import_functions' => true,
- ],
- ConstructorEmptyBracesFixer::name() => true,
- IssetToArrayKeyExistsFixer::name() => true,
- MultilineCommentOpeningClosingAloneFixer::name() => true,
- MultilinePromotedPropertiesFixer::name() => true,
- PhpUnitAssertArgumentsOrderFixer::name() => true,
- PhpdocNoSuperfluousParamFixer::name() => true,
- PhpdocParamOrderFixer::name() => true,
- StringableInterfaceFixer::name() => true,
])
->setFinder($finder)
- ->registerCustomFixers(new PhpCsFixerCustomFixers\Fixers())
;
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..8722551
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,16 @@
+FROM php:8.1-cli
+RUN apt-get update \
+ && apt-get install -y \
+ libzip-dev \
+ unzip \
+ git \
+ wget \
+ && docker-php-ext-install -j$(nproc) bcmath sockets \
+ && pecl install xdebug \
+ && docker-php-ext-enable xdebug && \
+ curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
+
+WORKDIR /opt/project
+
+
+
diff --git a/bin/console.php b/bin/console.php
new file mode 100644
index 0000000..645f886
--- /dev/null
+++ b/bin/console.php
@@ -0,0 +1,10 @@
+run();
\ No newline at end of file
diff --git a/composer.json b/composer.json
index e2891ef..d825c55 100644
--- a/composer.json
+++ b/composer.json
@@ -2,17 +2,9 @@
"name": "neo4j/neo4j-bundle",
"description": "Symfony integration for Neo4j",
"type": "symfony-bundle",
- "keywords": ["neo4j"],
+ "keywords": ["neo4j", "symfony", "bundle", "graph", "database", "cypher"],
"license": "MIT",
"authors": [
- {
- "name": "Tobias Nyholm",
- "email": "tobias.nyholm@gmail.com"
- },
- {
- "name": "Xavier Coureau",
- "email": "xavier@pandawan-technology.com"
- },
{
"name": "Ghlen Nagels",
"email": "ghlen@nagels.tech"
@@ -20,11 +12,11 @@
],
"require": {
"php": ">=8.0",
- "laudis/neo4j-php-client": "dev-main",
+ "laudis/neo4j-php-client": "^3.0.5",
"twig/twig": "^3.0",
"ext-json": "*",
"symfony/dependency-injection": "^5.4 || ^6.0",
- "symfony/config": "^6.0"
+ "symfony/config": "^5.4 || ^6.0"
},
"require-dev": {
"matthiasnoback/symfony-dependency-injection-test": "^4.3",
@@ -35,9 +27,9 @@
"symfony/http-kernel": "^5.4 || ^6.0",
"symfony/test-pack": "^1.1",
"symfony/yaml": "^5.4 || ^6.0",
- "vimeo/psalm": "^5.12",
+ "vimeo/psalm": "^5.15.0",
"kubawerlos/php-cs-fixer-custom-fixers": "^3.0",
- "friendsofphp/php-cs-fixer": "^3.0",
+ "friendsofphp/php-cs-fixer": "^3.30",
"psalm/plugin-phpunit": "^0.18"
},
"autoload": {
@@ -55,5 +47,10 @@
"allow-plugins": {
"php-http/discovery": false
}
+ },
+ "scripts": {
+ "psalm": "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"
}
}
diff --git a/config/data-collector.xml b/config/data-collector.xml
deleted file mode 100644
index 2b6f64a..0000000
--- a/config/data-collector.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/config/services.php b/config/services.php
new file mode 100644
index 0000000..8ec9920
--- /dev/null
+++ b/config/services.php
@@ -0,0 +1,54 @@
+services();
+
+ $services->set('neo4j.event_handler', EventHandler::class)
+ ->autowire()
+ ->autoconfigure();
+
+ $services->set('neo4j.client_factory', ClientFactory::class)
+ ->args([
+ service('neo4j.event_handler'),
+ ]);
+
+ $services->set('neo4j.client', SymfonyClient::class)
+ ->factory([service('neo4j.client_factory'), 'create'])
+ ->public();
+
+ $services->set('neo4j.driver', Driver::class)
+ ->factory([service('neo4j.client'), 'getDriver'])
+ ->public();
+
+ $services->set('neo4j.session', Session::class)
+ ->factory([service('neo4j.driver'), 'createSession'])
+ ->share(false)
+ ->public();
+
+ $services->set('neo4j.transaction', TransactionInterface::class)
+ ->factory([service('neo4j.session'), 'beginTransaction'])
+ ->share(false)
+ ->public();
+
+ $services->alias(ClientInterface::class, 'neo4j.client');
+ $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/config/services.xml b/config/services.xml
deleted file mode 100644
index de3a028..0000000
--- a/config/services.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..ae9637b
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,33 @@
+version: '3.7'
+networks:
+ neo4j-symfony:
+
+services:
+ app:
+ user: ${UID-1000}:${GID-1000}
+ build:
+ context: .
+ ports:
+ - ${DOCKER_HOST_APP_PORT:-8000}:80
+ volumes:
+ - ./:/opt/project
+ environment:
+ - NEO4J_HOST=neo4j
+ - NEO4J_DATABASE=neo4j
+ - NEO4J_PORT=7687
+ - NEO4J_USER=neo4j
+ - NEO4J_PASSWORD=testtest
+ working_dir: /opt/project
+ networks:
+ - neo4j-symfony
+
+ neo4j:
+ environment:
+ - NEO4J_AUTH=neo4j/testtest
+ image: neo4j:5
+ ports:
+ - ${DOCKER_HOST_NEO4J_HTTP_PORT:-7474}:7474
+ - ${DOCKER_HOST_NEO4J_BOLT_PORT:-7687}:7687
+ networks:
+ - neo4j-symfony
+
diff --git a/docs/README.md b/docs/README.md
index 3750677..addc374 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -7,27 +7,46 @@
[](https://packagist.org/packages/neo4j/neo4j-bundle)
-## Install
+Installation
+============
-Via Composer
+Make sure Composer is installed globally, as explained in the
+[installation chapter](https://getcomposer.org/doc/00-intro.md)
+of the Composer documentation.
-``` bash
+Applications that use Symfony Flex
+----------------------------------
+
+Open a command console, enter your project directory and execute:
+
+```console
+$ composer require neo4j/neo4j-bundle
+```
+
+Applications that don't use Symfony Flex
+----------------------------------------
+
+### Step 1: Download the Bundle
+
+Open a command console, enter your project directory and execute the
+following command to download the latest stable version of this bundle:
+
+```console
$ composer require neo4j/neo4j-bundle
```
-Enable the bundle in your kernel:
+### Step 2: Enable the Bundle
+
+Then, enable the bundle by adding it to the list of registered bundles
+in the `config/bundles.php` file of your project:
-``` php
- ['all' => true],
+];
```
## Documentation
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 26ef520..9fd609d 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -2,10 +2,10 @@
- ./Tests/Unit
+ ./tests/Unit
- ./Tests/Functional
+ ./tests/Functional
diff --git a/psalm.xml b/psalm.xml
index 94be006..ce09bf1 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -10,6 +10,7 @@
>
+
diff --git a/public/.gitignore b/public/.gitignore
new file mode 100644
index 0000000..c96a04f
--- /dev/null
+++ b/public/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
diff --git a/src/ClientFactory.php b/src/ClientFactory.php
index df86407..6ae3078 100644
--- a/src/ClientFactory.php
+++ b/src/ClientFactory.php
@@ -4,18 +4,23 @@
namespace Neo4j\Neo4jBundle;
-use InvalidArgumentException;
use Laudis\Neo4j\Authentication\Authenticate;
use Laudis\Neo4j\ClientBuilder;
use Laudis\Neo4j\Common\Uri;
use Laudis\Neo4j\Contracts\AuthenticateInterface;
use Laudis\Neo4j\Databags\DriverConfiguration;
+use Laudis\Neo4j\Databags\HttpPsrBindings;
use Laudis\Neo4j\Databags\SessionConfiguration;
use Laudis\Neo4j\Databags\SslConfiguration;
+use Laudis\Neo4j\Databags\SummarizedResult;
use Laudis\Neo4j\Databags\TransactionConfiguration;
use Laudis\Neo4j\Enum\AccessMode;
use Laudis\Neo4j\Enum\SslMode;
+use Laudis\Neo4j\Types\CypherMap;
use Neo4j\Neo4jBundle\DependencyInjection\Configuration;
+use Psr\Http\Client\ClientInterface;
+use Psr\Http\Message\RequestFactoryInterface;
+use Psr\Http\Message\StreamFactoryInterface;
/**
* @psalm-import-type SessionConfigArray from Configuration
@@ -39,10 +44,15 @@ public function __construct(
private array|null $sessionConfiguration,
private array|null $transactionConfiguration,
private array $connections,
- ) {}
+ private ClientInterface|null $client,
+ private StreamFactoryInterface|null $streamFactory,
+ private RequestFactoryInterface|null $requestFactory,
+ ) {
+ }
public function create(): SymfonyClient
{
+ /** @var ClientBuilder> $builder */
$builder = ClientBuilder::create();
if ($this->driverConfig) {
@@ -66,13 +76,12 @@ public function create(): SymfonyClient
);
}
- /** @psalm-suppress InvalidArgument */
return new SymfonyClient($builder->build(), $this->eventHandler);
}
private function makeDriverConfig(): DriverConfiguration
{
- return new DriverConfiguration(
+ $config = new DriverConfiguration(
userAgent: $this->driverConfig['user_agent'] ?? null,
httpPsrBindings: null,
sslConfig: $this->makeSslConfig($this->driverConfig['ssl'] ?? null),
@@ -81,6 +90,21 @@ private function makeDriverConfig(): DriverConfiguration
acquireConnectionTimeout: $this->driverConfig['acquire_connection_timeout'] ?? null,
semaphore: null,
);
+
+ $bindings = new HttpPsrBindings();
+ if ($this->client) {
+ $config = $config->withHttpPsrBindings($bindings->withClient($this->client));
+ }
+
+ if ($this->streamFactory) {
+ $config = $config->withHttpPsrBindings($bindings->withStreamFactory($this->streamFactory));
+ }
+
+ if ($this->requestFactory) {
+ $config = $config->withHttpPsrBindings($bindings->withRequestFactory($this->requestFactory));
+ }
+
+ return $config;
}
private function makeSessionConfig(): SessionConfiguration
@@ -107,19 +131,19 @@ private function makeTransactionConfig(): TransactionConfiguration
*/
private function createAuth(array|null $auth, string $dsn): AuthenticateInterface
{
- if ($auth === null) {
+ if (null === $auth) {
return Authenticate::disabled();
}
return match ($auth['type'] ?? null) {
'basic' => Authenticate::basic(
- $auth['username'] ?? throw new InvalidArgumentException('Missing username for basic authentication'),
- $auth['password'] ?? throw new InvalidArgumentException('Missing password for basic authentication')
+ $auth['username'] ?? throw new \InvalidArgumentException('Missing username for basic authentication'),
+ $auth['password'] ?? throw new \InvalidArgumentException('Missing password for basic authentication')
),
- 'kerberos' => Authenticate::kerberos($auth['token'] ?? throw new InvalidArgumentException('Missing token for kerberos authentication')),
+ 'kerberos' => Authenticate::kerberos($auth['token'] ?? throw new \InvalidArgumentException('Missing token for kerberos authentication')),
'dsn', null => Authenticate::fromUrl(Uri::create($dsn)),
'none' => Authenticate::disabled(),
- 'oid' => Authenticate::oidc($auth['token'] ?? throw new InvalidArgumentException('Missing token for oid authentication')),
+ 'oid' => Authenticate::oidc($auth['token'] ?? throw new \InvalidArgumentException('Missing token for oid authentication')),
};
}
@@ -128,7 +152,7 @@ private function createAuth(array|null $auth, string $dsn): AuthenticateInterfac
*/
private function makeSslConfig(array|null $ssl): SslConfiguration
{
- if ($ssl === null) {
+ if (null === $ssl) {
return new SslConfiguration(
mode: SslMode::DISABLE(),
verifyPeer: false,
diff --git a/src/Collector/Neo4jDataCollector.php b/src/Collector/Neo4jDataCollector.php
index 10ffdbf..9014743 100644
--- a/src/Collector/Neo4jDataCollector.php
+++ b/src/Collector/Neo4jDataCollector.php
@@ -4,69 +4,80 @@
namespace Neo4j\Neo4jBundle\Collector;
+use Laudis\Neo4j\Databags\ResultSummary;
+use Neo4j\Neo4jBundle\EventListener\Neo4jProfileListener;
+use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpKernel\DataCollector\DataCollector;
-use Throwable;
/**
- * @author Xavier Coureau
+ * @var array{
+ * successful_statements: array>,
+ * failed_statements: list
+ * } $data
*/
-final class Neo4jDataCollector extends DataCollector
+final class Neo4jDataCollector extends AbstractDataCollector
{
public function __construct(
- private QueryLogger $queryLogger
- ) {}
-
- public function collect(Request $request, Response $response, Throwable $exception = null): void
- {
- $this->data['time'] = $this->queryLogger->getElapsedTime();
- $this->data['nb_queries'] = count($this->queryLogger);
- $this->data['statements'] = $this->queryLogger->getStatements();
- $this->data['failed_statements'] = array_filter($this->queryLogger->getStatements(), static function ($statement) {
- return empty($statement['success']);
- });
+ private Neo4jProfileListener $subscriber
+ ) {
}
- public function reset(): void
+ public function collect(Request $request, Response $response, \Throwable $exception = null): void
{
- $this->data = [];
- $this->queryLogger->reset();
+ $this->data['successful_statements'] = array_map(
+ static fn (ResultSummary $summary) => $summary->toArray(),
+ $this->subscriber->getProfiledSummaries()
+ );
+
+ $this->data['failed_statements'] = array_map(
+ static fn (array $x) => [
+ '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()
+ );
}
- public function getQueryCount(): int
+ public function reset(): void
{
- return $this->data['nb_queries'];
+ parent::reset();
+ $this->subscriber->reset();
}
- /**
- * Return all statements, successful and not successful.
- */
- public function getStatements(): array
+ public function getName(): string
{
- return $this->data['statements'];
+ return 'neo4j';
}
- /**
- * Return not successful statements.
- */
public function getFailedStatements(): array
{
return $this->data['failed_statements'];
}
- public function getTime(): float
+ public function getSuccessfulStatements(): array
{
- return $this->data['time'];
+ return $this->data['successful_statements'];
}
- public function getTimeForQuery(): float
+ public function getQueryCount(): int
{
- return $this->data['time'];
+ return count($this->data['successful_statements']) + count($this->data['failed_statements']);
}
- public function getName(): string
+ public static function getTemplate(): ?string
{
- return 'neo4j';
+ return 'web_profiler.html.twig';
}
}
diff --git a/src/Collector/QueryLogger.php b/src/Collector/QueryLogger.php
deleted file mode 100644
index 3bca3a3..0000000
--- a/src/Collector/QueryLogger.php
+++ /dev/null
@@ -1,129 +0,0 @@
-
- *
- * @psalm-type StatementInfo = array{
- * start_time: float,
- * query: string,
- * parameters: string,
- * end_time: float,
- * nb_results: int,
- * statistics: array,
- * scheme: string,
- * success: bool,
- * exceptionCode: string,
- * exceptionMessage: string
- * }
- */
-class QueryLogger implements Countable
-{
- private int $nbQueries = 0;
-
- /**
- * @var list
- */
- private array $statements = [];
-
- public function record(Statement $statement): void
- {
- $statementText = $statement->getText();
- $statementParams = json_encode($statement->getParameters(), JSON_THROW_ON_ERROR);
-
- $this->statements[] = [
- 'start_time' => microtime(true) * 1000,
- 'query' => $statementText,
- 'parameters' => $statementParams,
-
- // Add dummy data in case we never run logException or finish
- 'end_time' => microtime(true) * 1000, // same
- 'nb_results' => 0,
- 'statistics' => [],
- 'scheme' => '',
- 'success' => false,
- 'exceptionCode' => '',
- 'exceptionMessage' => '',
- ];
- }
-
- /**
- * @param SummarizedResult $result
- *
- * @throws Exception
- */
- public function finish(SummarizedResult $result): void
- {
- $id = count($this->statements) - 1;
- if ($id < 0) {
- return;
- }
-
- $summary = $result->getSummary();
- $this->statements[$id] = array_merge($this->statements[$id], [
- 'end_time' => $summary->getResultConsumedAfter(),
- 'nb_results' => $result->count(),
- 'statistics' => iterator_to_array($summary->getCounters()->getIterator(), true),
- 'scheme' => $summary->getServerInfo()->getAddress()->getScheme(),
- 'success' => true,
- ]);
- }
-
- public function reset(): void
- {
- $this->statements = [];
- }
-
- public function logException(Neo4jException $exception): void
- {
- $classification = explode('.', $exception->getNeo4jCode())[1] ?? '';
- $idx = count($this->statements) - 1;
- if ($idx < 0) {
- return;
- }
-
- $this->statements[$idx] = array_merge($this->statements[$idx], [
- 'end_time' => microtime(true) * 1000,
- 'exceptionCode' => $classification,
- 'exceptionMessage' => $exception->getErrors()[0]->getMessage() ?? '',
- 'success' => false,
- ]);
- }
-
- public function count(): int
- {
- return $this->nbQueries;
- }
-
- /**
- * @return array
- */
- public function getStatements(): array
- {
- return $this->statements;
- }
-
- public function getElapsedTime(): float
- {
- $time = 0;
-
- foreach ($this->statements as $statement) {
- $time += $statement['end_time'] - $statement['start_time'];
- }
-
- return $time;
- }
-}
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index dad118d..a713ba2 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -20,6 +20,7 @@
* verify_peer?: bool|null,
* }
* @psalm-type DriverConfigArray = array{
+ * profiling?: bool,
* acquire_connection_timeout?: int|null,
* user_agent?: string|null,
* pool_size?: int|null,
@@ -61,16 +62,6 @@ public function getConfigTreeBuilder(): TreeBuilder
$treeBuilder->getRootNode()
->fixXmlConfig('driver')
->children()
- ->arrayNode('profiling')
- ->info('Profiling configuration')
- ->canBeEnabled()
- ->children()
- ->booleanNode('enabled')
- ->info('Enable profiling')
- ->defaultTrue()
- ->end()
- ->end()
- ->end()
->append($this->decorateDriverConfig())
->append($this->decorateSessionConfig())
->append($this->decorateTransactionConfig())
@@ -88,6 +79,10 @@ public function getConfigTreeBuilder(): TreeBuilder
->info('The alias for this driver. Default is "default".')
->defaultValue('default')
->end()
+ ->scalarNode('profiling')
+ ->info('Enable profiling for requests on this driver. If no value is provided the default value will be equal to the kernel.debug parameter.')
+ ->defaultValue(null)
+ ->end()
->scalarNode('dsn')
->info('The DSN for the driver. Default is "bolt://localhost:7687".')
->defaultValue('bolt://localhost:7687')
@@ -121,6 +116,7 @@ private function decorateSessionConfig(): ArrayNodeDefinition
->info('The default configuration for every session')
->children()
->scalarNode('fetch_size')
+ ->info('The amount of rows that are being fetched at once in the result cursor')
->end()
->enumNode('access_mode')
->values(['read', 'write', null])
diff --git a/src/DependencyInjection/Neo4jExtension.php b/src/DependencyInjection/Neo4jExtension.php
index 1677d86..eee991d 100644
--- a/src/DependencyInjection/Neo4jExtension.php
+++ b/src/DependencyInjection/Neo4jExtension.php
@@ -4,39 +4,66 @@
namespace Neo4j\Neo4jBundle\DependencyInjection;
+use Neo4j\Neo4jBundle\Collector\Neo4jDataCollector;
+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\Loader\XmlFileLoader;
-use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
+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;
/**
* @psalm-import-type NormalisedDriverConfig from Configuration
*/
-class Neo4jExtension extends ConfigurableExtension
+class Neo4jExtension extends Extension
{
- protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void
+ public function load(array $configs, ContainerBuilder $container): ContainerBuilder
{
- $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../../config'));
- $loader->load('services.xml');
- $loader->load('data-collector.xml');
-
- $debug = $container->getParameter('kernel.debug');
- if ($debug === false) {
- $container->removeDefinition('neo4j.collector.debug_collector');
- } elseif (($mergedConfig['profiling']['enabled'] ?? null) === false) {
- $container->removeDefinition('neo4j.collector.debug_collector');
- }
+ $configuration = new Configuration();
+ $mergedConfig = $this->processConfiguration($configuration, $configs);
+
+ $loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../../config'));
+ $loader->load('services.php');
$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, new Reference(ClientInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE))
+ ->setArgument(6, new Reference(StreamFactoryInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE))
+ ->setArgument(7, new Reference(RequestFactoryInterface::class, ContainerInterface::NULL_ON_INVALID_REFERENCE))
->setAbstract(false)
;
$container->getDefinition('neo4j.driver')
->setArgument(0, $mergedConfig['drivers']['alias'] ?? 'default');
+
+ $enabledProfiles = [];
+ foreach ($mergedConfig['drivers'] as $driver) {
+ 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))
+ ->setArgument(0, new Reference('neo4j.client_factory'))
+ ->addTag('data_collector')
+ );
+
+ $container->setDefinition('neo4j.subscriber', (new Definition(Neo4jProfileListener::class))
+ ->setArgument(0, $enabledProfiles)
+ ->addTag('kernel.event_subscriber')
+ );
+ }
+
+ return $container;
}
public function getConfiguration(array $config, ContainerBuilder $container): Configuration
diff --git a/src/Events/FailureEvent.php b/src/Event/FailureEvent.php
similarity index 60%
rename from src/Events/FailureEvent.php
rename to src/Event/FailureEvent.php
index d7447e7..f5e1c61 100644
--- a/src/Events/FailureEvent.php
+++ b/src/Event/FailureEvent.php
@@ -2,8 +2,9 @@
declare(strict_types=1);
-namespace Neo4j\Neo4jBundle\Events;
+namespace Neo4j\Neo4jBundle\Event;
+use Laudis\Neo4j\Databags\Statement;
use Laudis\Neo4j\Exception\Neo4jException;
use Symfony\Contracts\EventDispatcher\Event;
@@ -11,13 +12,10 @@ class FailureEvent extends Event
{
public const EVENT_ID = 'neo4j.on_failure';
- protected Neo4jException $exception;
-
protected bool $shouldThrowException = true;
- public function __construct(Neo4jException $exception)
+ public function __construct(private string|null $alias, private Statement $statement, private Neo4jException $exception)
{
- $this->exception = $exception;
}
public function getException(): Neo4jException
@@ -34,4 +32,14 @@ public function shouldThrowException(): bool
{
return $this->shouldThrowException;
}
+
+ public function getAlias(): string|null
+ {
+ return $this->alias;
+ }
+
+ public function getStatement(): Statement
+ {
+ return $this->statement;
+ }
}
diff --git a/src/Event/PostRunEvent.php b/src/Event/PostRunEvent.php
new file mode 100644
index 0000000..75e9a96
--- /dev/null
+++ b/src/Event/PostRunEvent.php
@@ -0,0 +1,29 @@
+result;
+ }
+
+ public function getAlias(): string|null
+ {
+ return $this->alias;
+ }
+}
diff --git a/src/Event/PreRunEvent.php b/src/Event/PreRunEvent.php
new file mode 100644
index 0000000..cd59239
--- /dev/null
+++ b/src/Event/PreRunEvent.php
@@ -0,0 +1,27 @@
+statement;
+ }
+
+ public function getAlias(): string|null
+ {
+ return $this->alias;
+ }
+}
diff --git a/src/EventHandler.php b/src/EventHandler.php
index e2e5247..34579c5 100644
--- a/src/EventHandler.php
+++ b/src/EventHandler.php
@@ -4,14 +4,20 @@
namespace Neo4j\Neo4jBundle;
+use Laudis\Neo4j\Common\Uri;
+use Laudis\Neo4j\Databags\DatabaseInfo;
+use Laudis\Neo4j\Databags\ResultSummary;
+use Laudis\Neo4j\Databags\ServerInfo;
use Laudis\Neo4j\Databags\Statement;
use Laudis\Neo4j\Databags\SummarizedResult;
+use Laudis\Neo4j\Databags\SummaryCounters;
+use Laudis\Neo4j\Enum\ConnectionProtocol;
+use Laudis\Neo4j\Enum\QueryTypeEnum;
use Laudis\Neo4j\Exception\Neo4jException;
use Laudis\Neo4j\Types\CypherList;
-use Laudis\Neo4j\Types\CypherMap;
-use Neo4j\Neo4jBundle\Events\FailureEvent;
-use Neo4j\Neo4jBundle\Events\PostRunEvent;
-use Neo4j\Neo4jBundle\Events\PreRunEvent;
+use Neo4j\Neo4jBundle\Event\FailureEvent;
+use Neo4j\Neo4jBundle\Event\PostRunEvent;
+use Neo4j\Neo4jBundle\Event\PreRunEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class EventHandler
@@ -24,31 +30,47 @@ public function __construct(?EventDispatcherInterface $dispatcher)
}
/**
- * @param callable():CypherList> $runHandler
- * @param iterable $statements
+ * @template T
*
- * @return CypherList>
+ * @param callable(Statement):SummarizedResult $runHandler
+ *
+ * @return SummarizedResult
*/
- public function handle(callable $runHandler, iterable $statements): CypherList
+ public function handle(callable $runHandler, Statement $statement, string|null $alias): SummarizedResult
{
if (null === $this->dispatcher) {
- return $runHandler();
+ return $runHandler($statement);
}
- $this->dispatcher->dispatch(new PreRunEvent($statements), PreRunEvent::EVENT_ID);
+ $this->dispatcher->dispatch(new PreRunEvent($alias, $statement), PreRunEvent::EVENT_ID);
try {
- $tbr = $runHandler();
- $this->dispatcher->dispatch(new PostRunEvent($tbr), PostRunEvent::EVENT_ID);
+ $tbr = $runHandler($statement);
+ $this->dispatcher->dispatch(new PostRunEvent($alias, $tbr->getSummary()), PostRunEvent::EVENT_ID);
} catch (Neo4jException $e) {
- $event = new FailureEvent($e);
+ $event = new FailureEvent($alias, $statement, $e);
$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'),
+ );
+
+ $tbr = new SummarizedResult($summary);
}
- return new CypherList();
+ return $tbr;
}
}
diff --git a/src/EventListener/Neo4jProfileListener.php b/src/EventListener/Neo4jProfileListener.php
new file mode 100644
index 0000000..b046b99
--- /dev/null
+++ b/src/EventListener/Neo4jProfileListener.php
@@ -0,0 +1,78 @@
+
+ */
+ private array $profiledSummaries = [];
+
+ /**
+ * @var list
+ */
+ private array $profiledFailures = [];
+
+ /**
+ * @param list $enabledProfiles
+ */
+ public function __construct(private 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)) {
+ $this->profiledSummaries[] = $event->getResult();
+ }
+ }
+
+ public function onFailure(FailureEvent $event): void
+ {
+ if (in_array($event->getAlias(), $this->enabledProfiles)) {
+ $this->profiledFailures[] = [
+ 'exception' => $event->getException(),
+ 'statement' => $event->getStatement(),
+ 'alias' => $event->getAlias(),
+ ];
+ }
+ }
+
+ 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/src/EventSubscriber/LoggerSubscriber.php b/src/EventSubscriber/LoggerSubscriber.php
deleted file mode 100644
index 88c9130..0000000
--- a/src/EventSubscriber/LoggerSubscriber.php
+++ /dev/null
@@ -1,49 +0,0 @@
-queryLogger = $queryLogger;
- }
-
- public static function getSubscribedEvents(): array
- {
- return [
- PreRunEvent::EVENT_ID => 'onPreRun',
- PostRunEvent::EVENT_ID => 'onPostRun',
- FailureEvent::EVENT_ID => 'onFailure',
- ];
- }
-
- public function onPreRun(PreRunEvent $event): void
- {
- foreach ($event->getStatements() as $statement) {
- $this->queryLogger->record($statement);
- }
- }
-
- public function onPostRun(PostRunEvent $event): void
- {
- foreach ($event->getResults() as $result) {
- $this->queryLogger->finish($result);
- }
- }
-
- public function onFailure(FailureEvent $event): void
- {
- $this->queryLogger->logException($event->getException());
- }
-}
diff --git a/src/Events/PostRunEvent.php b/src/Events/PostRunEvent.php
deleted file mode 100644
index dea501d..0000000
--- a/src/Events/PostRunEvent.php
+++ /dev/null
@@ -1,30 +0,0 @@
-> $results
- */
- public function __construct(
- protected CypherList $results
- ) {}
-
- /**
- * @return CypherList>
- */
- public function getResults(): CypherList
- {
- return $this->results;
- }
-}
diff --git a/src/Events/PreRunEvent.php b/src/Events/PreRunEvent.php
deleted file mode 100644
index e10ec55..0000000
--- a/src/Events/PreRunEvent.php
+++ /dev/null
@@ -1,34 +0,0 @@
-
- */
- private iterable $statements;
-
- /**
- * @param iterable $statements
- */
- public function __construct(iterable $statements)
- {
- $this->statements = $statements;
- }
-
- /**
- * @return iterable
- */
- public function getStatements(): iterable
- {
- return $this->statements;
- }
-}
diff --git a/src/Neo4jBundle.php b/src/Neo4jBundle.php
index 1e5e711..03973ae 100644
--- a/src/Neo4jBundle.php
+++ b/src/Neo4jBundle.php
@@ -6,9 +6,6 @@
use Symfony\Component\HttpKernel\Bundle\Bundle;
-/**
- * @author Tobias Nyholm
- */
class Neo4jBundle extends Bundle
{
}
diff --git a/src/Resources/public/css/neo4j.css b/src/Resources/public/css/neo4j.css
new file mode 100644
index 0000000..96de3b8
--- /dev/null
+++ b/src/Resources/public/css/neo4j.css
@@ -0,0 +1,3 @@
+.bg-red {
+ background-color: #B0413E;
+}
diff --git a/src/Resources/public/images/neo4j.svg b/src/Resources/public/images/neo4j.svg
new file mode 100644
index 0000000..e77bcaa
--- /dev/null
+++ b/src/Resources/public/images/neo4j.svg
@@ -0,0 +1,15 @@
+
diff --git a/tests/App/config/routing.yml b/src/Resources/public/js/neo4j.js
similarity index 100%
rename from tests/App/config/routing.yml
rename to src/Resources/public/js/neo4j.js
diff --git a/src/SymfonyClient.php b/src/SymfonyClient.php
index 6ea616b..efceb9d 100644
--- a/src/SymfonyClient.php
+++ b/src/SymfonyClient.php
@@ -27,7 +27,8 @@ class SymfonyClient implements ClientInterface
public function __construct(
private ClientInterface $client,
private EventHandler $handler
- ) {}
+ ) {
+ }
public function run(string $statement, iterable $parameters = [], string $alias = null): ?SummarizedResult
{
@@ -36,22 +37,28 @@ public function run(string $statement, iterable $parameters = [], string $alias
public function runStatement(Statement $statement, string $alias = null): ?SummarizedResult
{
- return $this->runStatements([$statement], $alias)->first();
+ return $this->handler->handle(fn (Statement $statement) => $this->client->runStatement($statement, $alias), $statement, $alias);
}
public function runStatements(iterable $statements, string $alias = null): CypherList
{
- return $this->handler->handle(fn () => $this->client->runStatements($statements, $alias), $statements);
+ $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);
- /**
- * @var callable():CypherList> $runHandler
- */
- $runHandler = fn (): CypherList => $tsx->runStatements($statements ?? []);
- $this->handler->handle($runHandler, $statements ?? []);
+ $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);
+ }
return $tsx;
}
@@ -67,7 +74,7 @@ public function writeTransaction(callable $tsxHandler, string $alias = null, Tra
$session = $this->client->getDriver($alias)->createSession($sessionConfig);
return TransactionHelper::retry(
- fn () => new SymfonyTransaction($session->beginTransaction([], $config), $this->handler),
+ fn () => new SymfonyTransaction($session->beginTransaction([], $config), $this->handler, $alias),
$tsxHandler
);
}
@@ -78,7 +85,7 @@ public function readTransaction(callable $tsxHandler, string $alias = null, Tran
$session = $this->client->getDriver($alias)->createSession($sessionConfig);
return TransactionHelper::retry(
- fn () => new SymfonyTransaction($session->beginTransaction([], $config), $this->handler),
+ fn () => new SymfonyTransaction($session->beginTransaction([], $config), $this->handler, $alias),
$tsxHandler
);
}
@@ -107,4 +114,9 @@ public function rollbackBoundTransaction(string $alias = null, int $depth = 1):
{
$this->client->rollbackBoundTransaction($alias, $depth);
}
+
+ public function hasDriver(string $alias): bool
+ {
+ return $this->client->hasDriver($alias);
+ }
}
diff --git a/src/SymfonyTransaction.php b/src/SymfonyTransaction.php
index 493841b..fee502b 100644
--- a/src/SymfonyTransaction.php
+++ b/src/SymfonyTransaction.php
@@ -15,17 +15,11 @@
*/
class SymfonyTransaction implements UnmanagedTransactionInterface
{
- /** @var UnmanagedTransactionInterface> */
- private UnmanagedTransactionInterface $tsx;
- private EventHandler $handler;
-
/**
* @param UnmanagedTransactionInterface> $tsx
*/
- public function __construct(UnmanagedTransactionInterface $tsx, EventHandler $handler)
+ public function __construct(private UnmanagedTransactionInterface $tsx, private EventHandler $handler, private string|null $alias)
{
- $this->tsx = $tsx;
- $this->handler = $handler;
}
public function run(string $statement, iterable $parameters = []): SummarizedResult
@@ -35,7 +29,7 @@ public function run(string $statement, iterable $parameters = []): SummarizedRes
public function runStatement(Statement $statement): SummarizedResult
{
- return $this->runStatements([$statement])->first();
+ return $this->handler->handle(fn ($statement) => $this->tsx->runStatement($statement), $statement, $this->alias);
}
/**
@@ -43,12 +37,21 @@ public function runStatement(Statement $statement): SummarizedResult
*/
public function runStatements(iterable $statements): CypherList
{
- return $this->handler->handle(fn () => $this->tsx->runStatements($statements), $statements);
+ $tbr = [];
+ foreach ($statements as $statement) {
+ $tbr[] = $this->runStatement($statement);
+ }
+
+ return CypherList::fromIterable($tbr);
}
public function commit(iterable $statements = []): CypherList
{
- return $this->handler->handle(fn () => $this->tsx->commit($statements), $statements);
+ $tbr = $this->runStatements($statements);
+
+ $this->tsx->commit();
+
+ return $tbr;
}
public function rollback(): void
diff --git a/views/webprofiler.html.twig b/templates/webprofiler.html.twig
similarity index 94%
rename from views/webprofiler.html.twig
rename to templates/webprofiler.html.twig
index ac0d985..8393225 100644
--- a/views/webprofiler.html.twig
+++ b/templates/webprofiler.html.twig
@@ -1,9 +1,10 @@
+{# templates/data_collector/template.html.twig #}
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% if collector.queryCount > 0 %}
{% set icon %}
- {{ include('@Neo4j/Icon/neo4j.svg') }}
+ {{ include('@Neo4j/neo4j.svg') }}
{{ collector.queryCount }}
stmt.
{% endset %}
@@ -30,8 +31,8 @@
{% endblock %}
{% block head %}
-
-
+
+
{{ parent() }}
{% endblock %}
@@ -62,7 +63,7 @@
- {% for idx, statement in collector.statements %}
+ {% for idx, statement in collector.successfulStatements %}
{% set start_time = statement.start_time|default(null) %}
{% set end_time = statement.end_time|default(null) %}
diff --git a/tests/App/config/default.yml b/tests/App/config/default.yml
index 17c918b..45c21a9 100644
--- a/tests/App/config/default.yml
+++ b/tests/App/config/default.yml
@@ -1,6 +1,6 @@
-imports:
- - { resource: framework.yml }
- - { resource: services.yml }
+framework:
+ secret: test
+ test: true
neo4j:
default_driver: neo4j-test
diff --git a/tests/App/config/framework.yml b/tests/App/config/framework.yml
deleted file mode 100644
index 5cfe332..0000000
--- a/tests/App/config/framework.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-framework:
- secret: test
- test: ~
- session:
-# storage_id: session.storage.mock_file
- storage_factory_id: session.storage.factory.native
- form: false
- csrf_protection: false
- validation:
- enabled: false
- router:
- resource: "%kernel.project_dir%/config/routing.yml"
- utf8: true
diff --git a/tests/App/config/services.yml b/tests/App/config/services.yml
deleted file mode 100644
index 0baad47..0000000
--- a/tests/App/config/services.yml
+++ /dev/null
@@ -1 +0,0 @@
-services:
diff --git a/tests/Functional/BundleInitializationTest.php b/tests/Functional/BundleInitializationTest.php
index fe43526..ff3ffc6 100644
--- a/tests/Functional/BundleInitializationTest.php
+++ b/tests/Functional/BundleInitializationTest.php
@@ -6,7 +6,6 @@
use Laudis\Neo4j\Contracts\ClientInterface;
use Neo4j\Neo4jBundle\Tests\App\TestKernel;
-use RuntimeException;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
@@ -40,7 +39,7 @@ public function testDefaultDsn(): void
$container = static::getContainer();
$client = $container->get('neo4j.client');
- $this->expectException(RuntimeException::class);
+ $this->expectException(\RuntimeException::class);
$this->expectExceptionMessage("Cannot connect to any server on alias: neo4j_undefined_configs with Uris: ('bolt://localhost')");
$client->getDriver('default');
}
@@ -50,7 +49,7 @@ public function testDsn(): void
static::bootKernel();
$container = static::getContainer();
- $this->expectException(RuntimeException::class);
+ $this->expectException(\RuntimeException::class);
$this->expectExceptionMessage("Cannot connect to any server on alias: neo4j_undefined_configs with Uris: ('bolt://localhost')");
$container->get('neo4j.driver');
diff --git a/tests/Unit/DependencyInjection/Neo4jExtensionTest.php b/tests/Unit/DependencyInjection/Neo4jExtensionTest.php
index 8c69e18..572535d 100644
--- a/tests/Unit/DependencyInjection/Neo4jExtensionTest.php
+++ b/tests/Unit/DependencyInjection/Neo4jExtensionTest.php
@@ -5,6 +5,7 @@
namespace Neo4j\Neo4jBundle\Tests\Unit\DependencyInjection;
use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase;
+use Neo4j\Neo4jBundle\Collector\Neo4jDataCollector;
use Neo4j\Neo4jBundle\DependencyInjection\Neo4jExtension;
/**
@@ -24,9 +25,7 @@ public function testDataCollectorLoaded(): void
$this->setParameter('kernel.debug', true);
$this->load();
- $this->assertContainerBuilderHasService('neo4j.collector.debug_collector',
- 'Neo4j\Neo4jBundle\Collector\Neo4jDataCollector'
- );
+ $this->assertContainerBuilderHasService('neo4j.data_collector', Neo4jDataCollector::class);
}
public function testDataCollectorNotLoadedInNonDebug(): void
@@ -34,15 +33,19 @@ public function testDataCollectorNotLoadedInNonDebug(): void
$this->setParameter('kernel.debug', false);
$this->load();
- $this->assertContainerBuilderNotHasService('neo4j.collector.debug_collector');
+ $this->assertContainerBuilderNotHasService('neo4j.data_collector');
}
public function testDataCollectorNotLoadedWhenDisabled(): void
{
$this->setParameter('kernel.debug', true);
- $this->load(['profiling' => ['enabled' => false]]);
+ $this->load(['drivers' => [
+ 'default' => [
+ 'profiling' => false,
+ ],
+ ]]);
- $this->assertContainerBuilderNotHasService('neo4j.collector.debug_collector');
+ $this->assertContainerBuilderNotHasService('neo4j.neo4j_data_collector');
}
protected function getContainerExtensions(): array
diff --git a/views/Icon/neo4j.svg b/views/Icon/neo4j.svg
deleted file mode 100644
index ef6cbac..0000000
--- a/views/Icon/neo4j.svg
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-