diff --git a/.github/workflows/integration-test-cluster-neo4j-4.yml b/.github/workflows/integration-test-cluster-neo4j-4.yml
index 9aa390ac..62ebd992 100644
--- a/.github/workflows/integration-test-cluster-neo4j-4.yml
+++ b/.github/workflows/integration-test-cluster-neo4j-4.yml
@@ -21,6 +21,7 @@ jobs:
run: |
echo "CONNECTION=neo4j://neo4j:testtest@neo4j" > .env
- uses: hoverkraft-tech/compose-action@v2.0.2
+ name: Start services
with:
compose-file: './docker-compose-neo4j-4.yml'
up-flags: '--build --remove-orphans'
diff --git a/.github/workflows/integration-test-cluster-neo4j-5.yml b/.github/workflows/integration-test-cluster-neo4j-5.yml
index 58289707..37588031 100644
--- a/.github/workflows/integration-test-cluster-neo4j-5.yml
+++ b/.github/workflows/integration-test-cluster-neo4j-5.yml
@@ -21,6 +21,7 @@ jobs:
run: |
echo "CONNECTION=neo4j://neo4j:testtest@neo4j" > .env
- uses: hoverkraft-tech/compose-action@v2.0.2
+ name: Start services
with:
compose-file: './docker-compose.yml'
up-flags: '--build --remove-orphans'
diff --git a/.github/workflows/integration-test-single-server.yml b/.github/workflows/integration-test-single-server.yml
index bbd88a65..aa47b687 100644
--- a/.github/workflows/integration-test-single-server.yml
+++ b/.github/workflows/integration-test-single-server.yml
@@ -19,6 +19,7 @@ jobs:
run: |
echo "CONNECTION=neo4j://neo4j:testtest@neo4j" > .env
- uses: hoverkraft-tech/compose-action@v2.0.2
+ name: Start services
with:
compose-file: './docker-compose-neo4j-4.yml'
up-flags: '--build --remove-orphans'
@@ -44,6 +45,7 @@ jobs:
run: |
echo "CONNECTION=neo4j://neo4j:testtest@neo4j" > .env
- uses: hoverkraft-tech/compose-action@v2.0.2
+ name: Start services
with:
compose-file: './docker-compose.yml'
up-flags: '--build'
diff --git a/composer.json b/composer.json
index a1183e37..3dab5e11 100644
--- a/composer.json
+++ b/composer.json
@@ -40,7 +40,8 @@
"suggest": {
"ext-bcmath": "Needed to implement bolt protocol",
"ext-sysvsem": "Needed for enabling connection pooling between processes",
- "composer-runtime-api": "Install composer 2 for auto detection of version in user agent"
+ "composer-runtime-api": "Install composer 2 for auto detection of version in user agent",
+ "psr/log": "Needed to enable logging"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
@@ -51,12 +52,12 @@
"friendsofphp/php-cs-fixer": "3.15.0",
"psalm/plugin-phpunit": "^0.18",
"monolog/monolog": "^2.2",
- "psr/log": "^1.1",
"symfony/uid": "^5.0",
"symfony/var-dumper": "^5.0",
"cache/integration-tests": "dev-master",
"kubawerlos/php-cs-fixer-custom-fixers": "3.13.*",
- "rector/rector": "^1.0"
+ "rector/rector": "^1.0",
+ "psr/log": "^3.0"
},
"autoload": {
"psr-4": {
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index bd9af323..1f945826 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -12,6 +12,6 @@
-
+
diff --git a/src/Authentication/Authenticate.php b/src/Authentication/Authenticate.php
index 34b1a287..2b711146 100644
--- a/src/Authentication/Authenticate.php
+++ b/src/Authentication/Authenticate.php
@@ -15,6 +15,7 @@
use function explode;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Contracts\AuthenticateInterface;
use Psr\Http\Message\UriInterface;
@@ -32,9 +33,9 @@ final class Authenticate
*
* @pure
*/
- public static function basic(string $username, string $password): BasicAuth
+ public static function basic(string $username, string $password, ?Neo4jLogger $logger = null): BasicAuth
{
- return new BasicAuth($username, $password);
+ return new BasicAuth($username, $password, $logger);
}
/**
@@ -42,9 +43,9 @@ public static function basic(string $username, string $password): BasicAuth
*
* @pure
*/
- public static function kerberos(string $token): KerberosAuth
+ public static function kerberos(string $token, ?Neo4jLogger $logger = null): KerberosAuth
{
- return new KerberosAuth($token);
+ return new KerberosAuth($token, $logger);
}
/**
@@ -52,9 +53,9 @@ public static function kerberos(string $token): KerberosAuth
*
* @pure
*/
- public static function oidc(string $token): OpenIDConnectAuth
+ public static function oidc(string $token, ?Neo4jLogger $logger = null): OpenIDConnectAuth
{
- return new OpenIDConnectAuth($token);
+ return new OpenIDConnectAuth($token, $logger);
}
/**
@@ -62,9 +63,9 @@ public static function oidc(string $token): OpenIDConnectAuth
*
* @pure
*/
- public static function disabled(): NoAuth
+ public static function disabled(?Neo4jLogger $logger = null): NoAuth
{
- return new NoAuth();
+ return new NoAuth($logger);
}
/**
@@ -72,7 +73,7 @@ public static function disabled(): NoAuth
*
* @pure
*/
- public static function fromUrl(UriInterface $uri): AuthenticateInterface
+ public static function fromUrl(UriInterface $uri, ?Neo4jLogger $logger = null): AuthenticateInterface
{
/**
* @psalm-suppress ImpureMethodCall Uri is a pure object:
@@ -86,9 +87,9 @@ public static function fromUrl(UriInterface $uri): AuthenticateInterface
$explode = explode(':', $userInfo);
[$user, $pass] = $explode;
- return self::basic($user, $pass);
+ return self::basic($user, $pass, $logger);
}
- return self::disabled();
+ return self::disabled($logger);
}
}
diff --git a/src/Authentication/BasicAuth.php b/src/Authentication/BasicAuth.php
index 852060ff..675e667c 100644
--- a/src/Authentication/BasicAuth.php
+++ b/src/Authentication/BasicAuth.php
@@ -22,10 +22,12 @@
use Bolt\protocol\V5_3;
use Bolt\protocol\V5_4;
use Exception;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Common\ResponseHelper;
use Laudis\Neo4j\Contracts\AuthenticateInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
+use Psr\Log\LogLevel;
/**
* Authenticates connections using a basic username and password.
@@ -37,14 +39,13 @@ final class BasicAuth implements AuthenticateInterface
*/
public function __construct(
private readonly string $username,
- private readonly string $password
+ private readonly string $password,
+ private readonly ?Neo4jLogger $logger,
) {}
- /**
- * @psalm-mutation-free
- */
public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface
{
+ $this->logger?->log(LogLevel::DEBUG, 'Authenticating using BasicAuth');
$combo = base64_encode($this->username.':'.$this->password);
/**
@@ -64,8 +65,10 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s
public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array
{
if (method_exists($protocol, 'logon')) {
+ $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]);
$protocol->hello(['user_agent' => $userAgent]);
$response = ResponseHelper::getResponse($protocol);
+ $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'basic', 'principal' => $this->username]);
$protocol->logon([
'scheme' => 'basic',
'principal' => $this->username,
@@ -76,6 +79,7 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $
/** @var array{server: string, connection_id: string, hints: list} */
return $response->content;
} else {
+ $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent, 'scheme' => 'basic', 'principal' => $this->username]);
$protocol->hello([
'user_agent' => $userAgent,
'scheme' => 'basic',
diff --git a/src/Authentication/KerberosAuth.php b/src/Authentication/KerberosAuth.php
index 24c2d590..3c57d32f 100644
--- a/src/Authentication/KerberosAuth.php
+++ b/src/Authentication/KerberosAuth.php
@@ -20,10 +20,12 @@
use Bolt\protocol\V5_3;
use Bolt\protocol\V5_4;
use Exception;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Common\ResponseHelper;
use Laudis\Neo4j\Contracts\AuthenticateInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
+use Psr\Log\LogLevel;
use function sprintf;
@@ -36,14 +38,13 @@ final class KerberosAuth implements AuthenticateInterface
* @psalm-external-mutation-free
*/
public function __construct(
- private readonly string $token
+ private readonly string $token,
+ private readonly ?Neo4jLogger $logger,
) {}
- /**
- * @psalm-mutation-free
- */
public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface
{
+ $this->logger?->log(LogLevel::DEBUG, 'Authenticating using KerberosAuth');
/**
* @psalm-suppress ImpureMethodCall Request is a pure object:
*
@@ -61,8 +62,10 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s
public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array
{
if (method_exists($protocol, 'logon')) {
+ $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]);
$protocol->hello(['user_agent' => $userAgent]);
$response = ResponseHelper::getResponse($protocol);
+ $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'kerberos', 'principal' => '']);
$protocol->logon([
'scheme' => 'kerberos',
'principal' => '',
@@ -73,6 +76,7 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $
/** @var array{server: string, connection_id: string, hints: list} */
return $response->content;
} else {
+ $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent, 'scheme' => 'kerberos', 'principal' => '']);
$protocol->hello([
'user_agent' => $userAgent,
'scheme' => 'kerberos',
diff --git a/src/Authentication/NoAuth.php b/src/Authentication/NoAuth.php
index d277a3d5..51d246cb 100644
--- a/src/Authentication/NoAuth.php
+++ b/src/Authentication/NoAuth.php
@@ -20,10 +20,12 @@
use Bolt\protocol\V5_3;
use Bolt\protocol\V5_4;
use Exception;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Common\ResponseHelper;
use Laudis\Neo4j\Contracts\AuthenticateInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
+use Psr\Log\LogLevel;
use function sprintf;
@@ -33,10 +35,15 @@
final class NoAuth implements AuthenticateInterface
{
/**
- * @psalm-mutation-free
+ * @pure
*/
+ public function __construct(
+ private readonly ?Neo4jLogger $logger
+ ) {}
+
public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface
{
+ $this->logger?->log(LogLevel::DEBUG, 'Authentication disabled');
/**
* @psalm-suppress ImpureMethodCall Request is a pure object:
*
@@ -53,8 +60,10 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s
public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array
{
if (method_exists($protocol, 'logon')) {
+ $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]);
$protocol->hello(['user_agent' => $userAgent]);
$response = ResponseHelper::getResponse($protocol);
+ $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'none']);
$protocol->logon([
'scheme' => 'none',
]);
@@ -63,6 +72,7 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $
/** @var array{server: string, connection_id: string, hints: list} */
return $response->content;
} else {
+ $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent, 'scheme' => 'none']);
$protocol->hello([
'user_agent' => $userAgent,
'scheme' => 'none',
diff --git a/src/Authentication/OpenIDConnectAuth.php b/src/Authentication/OpenIDConnectAuth.php
index bc05d7cc..18b2b796 100644
--- a/src/Authentication/OpenIDConnectAuth.php
+++ b/src/Authentication/OpenIDConnectAuth.php
@@ -20,10 +20,12 @@
use Bolt\protocol\V5_3;
use Bolt\protocol\V5_4;
use Exception;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Common\ResponseHelper;
use Laudis\Neo4j\Contracts\AuthenticateInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
+use Psr\Log\LogLevel;
use function sprintf;
@@ -33,14 +35,13 @@ final class OpenIDConnectAuth implements AuthenticateInterface
* @psalm-external-mutation-free
*/
public function __construct(
- private readonly string $token
+ private readonly string $token,
+ private readonly ?Neo4jLogger $logger
) {}
- /**
- * @psalm-mutation-free
- */
public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface
{
+ $this->logger?->log(LogLevel::DEBUG, 'Authenticating using OpenIDConnectAuth');
/**
* @psalm-suppress ImpureMethodCall Request is a pure object:
*
@@ -58,8 +59,10 @@ public function authenticateHttp(RequestInterface $request, UriInterface $uri, s
public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $userAgent): array
{
if (method_exists($protocol, 'logon')) {
+ $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent]);
$protocol->hello(['user_agent' => $userAgent]);
$response = ResponseHelper::getResponse($protocol);
+ $this->logger?->log(LogLevel::DEBUG, 'LOGON', ['scheme' => 'bearer']);
$protocol->logon([
'scheme' => 'bearer',
'credentials' => $this->token,
@@ -69,6 +72,7 @@ public function authenticateBolt(V4_4|V5|V5_1|V5_2|V5_3|V5_4 $protocol, string $
/** @var array{server: string, connection_id: string, hints: list} */
return $response->content;
} else {
+ $this->logger?->log(LogLevel::DEBUG, 'HELLO', ['user_agent' => $userAgent, 'scheme' => 'bearer']);
$protocol->hello([
'user_agent' => $userAgent,
'scheme' => 'bearer',
diff --git a/src/Basic/Driver.php b/src/Basic/Driver.php
index ca76954d..7b73a876 100644
--- a/src/Basic/Driver.php
+++ b/src/Basic/Driver.php
@@ -57,4 +57,9 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co
return new self($driver);
}
+
+ public function closeConnections(): void
+ {
+ $this->driver->closeConnections();
+ }
}
diff --git a/src/Bolt/BoltConnection.php b/src/Bolt/BoltConnection.php
index dba4e9a2..7255b6c5 100644
--- a/src/Bolt/BoltConnection.php
+++ b/src/Bolt/BoltConnection.php
@@ -22,7 +22,9 @@
use Bolt\protocol\V5_2;
use Bolt\protocol\V5_3;
use Bolt\protocol\V5_4;
+use Exception;
use Laudis\Neo4j\Common\ConnectionConfiguration;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Contracts\AuthenticateInterface;
use Laudis\Neo4j\Contracts\ConnectionInterface;
use Laudis\Neo4j\Contracts\FormatterInterface;
@@ -33,10 +35,11 @@
use Laudis\Neo4j\Exception\Neo4jException;
use Laudis\Neo4j\Types\CypherList;
use Psr\Http\Message\UriInterface;
+use Psr\Log\LogLevel;
use WeakReference;
/**
- * @implements ConnectionInterface
+ * @implements ConnectionInterface
*
* @psalm-import-type BoltMeta from FormatterInterface
*/
@@ -58,7 +61,7 @@ class BoltConnection implements ConnectionInterface
private array $subscribedResults = [];
/**
- * @return array{0: V4_4|V5|V5_1|V5_2|V5_3|V5_4, 1: Connection}
+ * @return array{0: V4_4|V5|V5_1|V5_2|V5_3|V5_4|null, 1: Connection}
*/
public function getImplementation(): array
{
@@ -69,12 +72,13 @@ public function getImplementation(): array
* @psalm-mutation-free
*/
public function __construct(
- private V4_4|V5|V5_1|V5_2|V5_3|V5_4 $boltProtocol,
+ private V4_4|V5|V5_1|V5_2|V5_3|V5_4|null $boltProtocol,
private readonly Connection $connection,
private readonly AuthenticateInterface $auth,
private readonly string $userAgent,
/** @psalm-readonly */
- private readonly ConnectionConfiguration $config
+ private readonly ConnectionConfiguration $config,
+ private readonly ?Neo4jLogger $logger,
) {}
public function getEncryptionLevel(): string
@@ -135,12 +139,26 @@ public function getAuthentication(): AuthenticateInterface
return $this->auth;
}
- /**
- * @psalm-mutation-free
- */
public function isOpen(): bool
{
- return !in_array($this->protocol()->serverState, [ServerState::DISCONNECTED, ServerState::DEFUNCT], true);
+ if (!isset($this->boltProtocol)) {
+ return false;
+ }
+
+ return !in_array(
+ $this->protocol()->serverState,
+ [ServerState::DISCONNECTED, ServerState::DEFUNCT],
+ true
+ );
+ }
+
+ public function isStreaming(): bool
+ {
+ return in_array(
+ $this->protocol()->serverState,
+ [ServerState::STREAMING, ServerState::TX_STREAMING],
+ true
+ );
}
public function setTimeout(float $timeout): void
@@ -150,7 +168,8 @@ public function setTimeout(float $timeout): void
public function consumeResults(): void
{
- if ($this->protocol()->serverState !== ServerState::STREAMING && $this->protocol()->serverState !== ServerState::TX_STREAMING) {
+ $this->logger?->log(LogLevel::DEBUG, 'Consuming results');
+ if (!$this->isStreaming()) {
$this->subscribedResults = [];
return;
@@ -174,6 +193,7 @@ public function consumeResults(): void
*/
public function reset(): void
{
+ $this->logger?->log(LogLevel::DEBUG, 'RESET');
$response = $this->protocol()
->reset()
->getResponse();
@@ -191,6 +211,7 @@ public function begin(?string $database, ?float $timeout, BookmarkHolder $holder
$this->consumeResults();
$extra = $this->buildRunExtra($database, $timeout, $holder, AccessMode::WRITE());
+ $this->logger?->log(LogLevel::DEBUG, 'BEGIN', $extra);
$response = $this->protocol()
->begin($extra)
->getResponse();
@@ -205,6 +226,7 @@ public function begin(?string $database, ?float $timeout, BookmarkHolder $holder
public function discard(?int $qid): void
{
$extra = $this->buildResultExtra(null, $qid);
+ $this->logger?->log(LogLevel::DEBUG, 'DISCARD', $extra);
$response = $this->protocol()
->discard($extra)
->getResponse();
@@ -218,9 +240,16 @@ public function discard(?int $qid): void
*
* @return BoltMeta
*/
- public function run(string $text, array $parameters, ?string $database, ?float $timeout, BookmarkHolder $holder, ?AccessMode $mode): array
- {
+ public function run(
+ string $text,
+ array $parameters,
+ ?string $database,
+ ?float $timeout,
+ BookmarkHolder $holder,
+ ?AccessMode $mode
+ ): array {
$extra = $this->buildRunExtra($database, $timeout, $holder, $mode);
+ $this->logger?->log(LogLevel::DEBUG, 'RUN', $extra);
$response = $this->protocol()
->run($text, $parameters, $extra)
->getResponse();
@@ -236,6 +265,7 @@ public function run(string $text, array $parameters, ?string $database, ?float $
*/
public function commit(): void
{
+ $this->logger?->log(LogLevel::DEBUG, 'COMMIT');
$this->consumeResults();
$response = $this->protocol()
@@ -251,6 +281,7 @@ public function commit(): void
*/
public function rollback(): void
{
+ $this->logger?->log(LogLevel::DEBUG, 'ROLLBACK');
$this->consumeResults();
$response = $this->protocol()
@@ -261,6 +292,10 @@ public function rollback(): void
public function protocol(): V4_4|V5|V5_1|V5_2|V5_3|V5_4
{
+ if (!isset($this->boltProtocol)) {
+ throw new Exception('Connection is closed');
+ }
+
return $this->boltProtocol;
}
@@ -274,6 +309,7 @@ public function protocol(): V4_4|V5|V5_1|V5_2|V5_3|V5_4
public function pull(?int $qid, ?int $fetchSize): array
{
$extra = $this->buildResultExtra($fetchSize, $qid);
+ $this->logger?->log(LogLevel::DEBUG, 'PULL', $extra);
$tbr = [];
/** @var Response $response */
@@ -287,13 +323,19 @@ public function pull(?int $qid, ?int $fetchSize): array
}
public function __destruct()
+ {
+ $this->close();
+ }
+
+ public function close(): void
{
try {
if ($this->isOpen()) {
- if ($this->protocol()->serverState === ServerState::STREAMING || $this->protocol()->serverState === ServerState::TX_STREAMING) {
+ if ($this->isStreaming()) {
$this->consumeResults();
}
+ $this->logger?->log(LogLevel::DEBUG, 'GOODBYE');
$this->protocol()->goodbye();
unset($this->boltProtocol); // has to be set to null as the sockets don't recover nicely contrary to what the underlying code might lead you to believe;
@@ -339,6 +381,10 @@ private function buildResultExtra(?int $fetchSize, ?int $qid): array
public function getServerState(): string
{
+ if (!isset($this->boltProtocol)) {
+ return ServerState::DISCONNECTED->name;
+ }
+
return $this->protocol()->serverState->name;
}
@@ -355,6 +401,7 @@ public function getUserAgent(): string
private function assertNoFailure(Response $response): void
{
if ($response->signature === Signature::FAILURE) {
+ $this->logger?->log(LogLevel::ERROR, 'FAILURE');
$this->protocol()->reset()->getResponse(); // what if the reset fails? what should be expected behaviour?
throw Neo4jException::fromBoltResponse($response);
}
diff --git a/src/Bolt/BoltDriver.php b/src/Bolt/BoltDriver.php
index 23bb1bad..0fa16706 100644
--- a/src/Bolt/BoltDriver.php
+++ b/src/Bolt/BoltDriver.php
@@ -72,7 +72,7 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co
}
$configuration ??= DriverConfiguration::default();
- $authenticate ??= Authenticate::fromUrl($uri);
+ $authenticate ??= Authenticate::fromUrl($uri, $configuration->getLogger());
$semaphore = $configuration->getSemaphoreFactory()->create($uri, $configuration);
/** @psalm-suppress InvalidArgument */
@@ -90,7 +90,7 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co
*/
public function createSession(?SessionConfiguration $config = null): SessionInterface
{
- $sessionConfig = SessionConfiguration::fromUri($this->parsedUrl);
+ $sessionConfig = SessionConfiguration::fromUri($this->parsedUrl, $this->pool->getLogger());
if ($config !== null) {
$sessionConfig = $sessionConfig->merge($config);
}
@@ -109,4 +109,9 @@ public function verifyConnectivity(?SessionConfiguration $config = null): bool
return true;
}
+
+ public function closeConnections(): void
+ {
+ $this->pool->close();
+ }
}
diff --git a/src/Bolt/ConnectionPool.php b/src/Bolt/ConnectionPool.php
index d3d6556f..2b2bfa25 100644
--- a/src/Bolt/ConnectionPool.php
+++ b/src/Bolt/ConnectionPool.php
@@ -15,6 +15,7 @@
use Generator;
use Laudis\Neo4j\BoltFactory;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Contracts\AuthenticateInterface;
use Laudis\Neo4j\Contracts\ConnectionInterface;
use Laudis\Neo4j\Contracts\ConnectionPoolInterface;
@@ -41,21 +42,27 @@ final class ConnectionPool implements ConnectionPoolInterface
public function __construct(
private readonly SemaphoreInterface $semaphore,
private readonly BoltFactory $factory,
- private readonly ConnectionRequestData $data
+ private readonly ConnectionRequestData $data,
+ private readonly ?Neo4jLogger $logger
) {}
- public static function create(UriInterface $uri, AuthenticateInterface $auth, DriverConfiguration $conf, SemaphoreInterface $semaphore): self
- {
+ public static function create(
+ UriInterface $uri,
+ AuthenticateInterface $auth,
+ DriverConfiguration $conf,
+ SemaphoreInterface $semaphore
+ ): self {
return new self(
$semaphore,
- BoltFactory::create(),
+ BoltFactory::create($conf->getLogger()),
new ConnectionRequestData(
$uri->getHost(),
$uri,
$auth,
$conf->getUserAgent(),
$conf->getSslConfiguration()
- )
+ ),
+ $conf->getLogger()
);
}
@@ -106,6 +113,11 @@ public function release(ConnectionInterface $connection): void
}
}
+ public function getLogger(): ?Neo4jLogger
+ {
+ return $this->logger;
+ }
+
/**
* @return BoltConnection|null
*/
@@ -157,4 +169,12 @@ private function returnAnyAvailableConnection(SessionConfiguration $config): ?Co
return null;
}
+
+ public function close(): void
+ {
+ foreach ($this->activeConnections as $activeConnection) {
+ $activeConnection->close();
+ }
+ $this->activeConnections = [];
+ }
}
diff --git a/src/Bolt/Session.php b/src/Bolt/Session.php
index 0cf01535..84214e00 100644
--- a/src/Bolt/Session.php
+++ b/src/Bolt/Session.php
@@ -15,6 +15,7 @@
use Exception;
use Laudis\Neo4j\Common\GeneratorHelper;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Common\TransactionHelper;
use Laudis\Neo4j\Contracts\ConnectionPoolInterface;
use Laudis\Neo4j\Contracts\FormatterInterface;
@@ -30,6 +31,7 @@
use Laudis\Neo4j\Exception\Neo4jException;
use Laudis\Neo4j\Neo4j\Neo4jConnectionPool;
use Laudis\Neo4j\Types\CypherList;
+use Psr\Log\LogLevel;
/**
* A session using bolt connections.
@@ -65,6 +67,7 @@ public function runStatements(iterable $statements, ?TransactionConfiguration $c
{
$tbr = [];
+ $this->getLogger()?->log(LogLevel::INFO, 'Running statements', ['statements' => $statements]);
$config = $this->mergeTsxConfig($config);
foreach ($statements as $statement) {
$tbr[] = $this->beginInstantTransaction($this->config, $config)->runStatement($statement);
@@ -93,6 +96,7 @@ public function run(string $statement, iterable $parameters = [], ?TransactionCo
public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null)
{
+ $this->getLogger()?->log(LogLevel::INFO, 'Beginning write transaction', ['config' => $config]);
$config = $this->mergeTsxConfig($config);
return TransactionHelper::retry(
@@ -103,6 +107,7 @@ public function writeTransaction(callable $tsxHandler, ?TransactionConfiguration
public function readTransaction(callable $tsxHandler, ?TransactionConfiguration $config = null)
{
+ $this->getLogger()?->log(LogLevel::INFO, 'Beginning read transaction', ['config' => $config]);
$config = $this->mergeTsxConfig($config);
return TransactionHelper::retry(
@@ -118,6 +123,7 @@ public function transaction(callable $tsxHandler, ?TransactionConfiguration $con
public function beginTransaction(?iterable $statements = null, ?TransactionConfiguration $config = null): UnmanagedTransactionInterface
{
+ $this->getLogger()?->log(LogLevel::INFO, 'Beginning transaction', ['statements' => $statements, 'config' => $config]);
$config = $this->mergeTsxConfig($config);
$tsx = $this->startTransaction($config, $this->config);
@@ -133,6 +139,7 @@ private function beginInstantTransaction(
SessionConfiguration $config,
TransactionConfiguration $tsxConfig
): TransactionInterface {
+ $this->getLogger()?->log(LogLevel::INFO, 'Starting instant transaction', ['config' => $tsxConfig]);
$connection = $this->acquireConnection($tsxConfig, $config);
return new BoltUnmanagedTransaction($this->config->getDatabase(), $this->formatter, $connection, $this->config, $tsxConfig, $this->bookmarkHolder);
@@ -143,6 +150,7 @@ private function beginInstantTransaction(
*/
private function acquireConnection(TransactionConfiguration $config, SessionConfiguration $sessionConfig): BoltConnection
{
+ $this->getLogger()?->log(LogLevel::INFO, 'Acquiring connection', ['config' => $config, 'sessionConfig' => $sessionConfig]);
$connection = $this->pool->acquire($sessionConfig);
/** @var BoltConnection $connection */
$connection = GeneratorHelper::getReturnFromGenerator($connection);
@@ -160,6 +168,7 @@ private function acquireConnection(TransactionConfiguration $config, SessionConf
private function startTransaction(TransactionConfiguration $config, SessionConfiguration $sessionConfig): UnmanagedTransactionInterface
{
+ $this->getLogger()?->log(LogLevel::INFO, 'Starting transaction', ['config' => $config, 'sessionConfig' => $sessionConfig]);
try {
$connection = $this->acquireConnection($config, $sessionConfig);
@@ -183,4 +192,9 @@ public function getLastBookmark(): Bookmark
{
return $this->bookmarkHolder->getBookmark();
}
+
+ private function getLogger(): ?Neo4jLogger
+ {
+ return $this->pool->getLogger();
+ }
}
diff --git a/src/BoltFactory.php b/src/BoltFactory.php
index f0b4611f..bd0e555d 100644
--- a/src/BoltFactory.php
+++ b/src/BoltFactory.php
@@ -21,6 +21,7 @@
use Laudis\Neo4j\Bolt\SystemWideConnectionFactory;
use Laudis\Neo4j\Bolt\UriConfiguration;
use Laudis\Neo4j\Common\ConnectionConfiguration;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Contracts\BasicConnectionFactoryInterface;
use Laudis\Neo4j\Contracts\ConnectionInterface;
use Laudis\Neo4j\Databags\ConnectionRequestData;
@@ -40,12 +41,13 @@ class BoltFactory
public function __construct(
private readonly BasicConnectionFactoryInterface $connectionFactory,
private readonly ProtocolFactory $protocolFactory,
- private readonly SslConfigurationFactory $sslConfigurationFactory
+ private readonly SslConfigurationFactory $sslConfigurationFactory,
+ private readonly ?Neo4jLogger $logger = null
) {}
- public static function create(): self
+ public static function create(?Neo4jLogger $logger): self
{
- return new self(SystemWideConnectionFactory::getInstance(), new ProtocolFactory(), new SslConfigurationFactory());
+ return new self(SystemWideConnectionFactory::getInstance(), new ProtocolFactory(), new SslConfigurationFactory(), $logger);
}
public function createConnection(ConnectionRequestData $data, SessionConfiguration $sessionConfig): BoltConnection
@@ -73,11 +75,15 @@ public function createConnection(ConnectionRequestData $data, SessionConfigurati
$sslLevel
);
- return new BoltConnection($protocol, $connection, $data->getAuth(), $data->getUserAgent(), $config);
+ return new BoltConnection($protocol, $connection, $data->getAuth(), $data->getUserAgent(), $config, $this->logger);
}
public function canReuseConnection(ConnectionInterface $connection, ConnectionRequestData $data, SessionConfiguration $config): bool
{
+ if (!$connection->isOpen()) {
+ return false;
+ }
+
$databaseInfo = $connection->getDatabaseInfo();
$database = $databaseInfo?->getName();
diff --git a/src/ClientBuilder.php b/src/ClientBuilder.php
index f6c91caf..cf515701 100644
--- a/src/ClientBuilder.php
+++ b/src/ClientBuilder.php
@@ -52,7 +52,7 @@ public function __construct(
private SessionConfiguration $defaultSessionConfig,
/** @psalm-readonly */
private TransactionConfiguration $defaultTransactionConfig,
- private DriverSetupManager $driverSetups
+ private DriverSetupManager $driverSetups,
) {}
/**
@@ -80,7 +80,7 @@ public function withDriver(string $alias, string $url, ?AuthenticateInterface $a
{
$uri = Uri::create($url);
- $authentication ??= Authenticate::fromUrl($uri);
+ $authentication ??= Authenticate::fromUrl($uri, $this->driverSetups->getLogger());
return $this->withParsedUrl($alias, $uri, $authentication, $priority ?? 0);
}
diff --git a/src/Common/DriverSetupManager.php b/src/Common/DriverSetupManager.php
index e01e204d..a6ab7a85 100644
--- a/src/Common/DriverSetupManager.php
+++ b/src/Common/DriverSetupManager.php
@@ -113,7 +113,7 @@ public function getDriver(SessionConfiguration $config, ?string $alias = null):
/** @var SplPriorityQueue */
$this->driverSetups['default'] = new SplPriorityQueue();
- $setup = new DriverSetup(Uri::create(self::DEFAULT_DRIVER_CONFIG), Authenticate::disabled());
+ $setup = new DriverSetup(Uri::create(self::DEFAULT_DRIVER_CONFIG), Authenticate::disabled($config->getLogger()));
$this->driverSetups['default']->insert($setup, PHP_INT_MIN);
return $this->getDriver($config);
@@ -192,4 +192,12 @@ public function withFormatter(FormatterInterface $formatter): self
return $tbr;
}
+
+ /**
+ * @psalm-mutation-free
+ */
+ public function getLogger(): ?Neo4jLogger
+ {
+ return $this->configuration->getLogger();
+ }
}
diff --git a/src/Common/Neo4jLogger.php b/src/Common/Neo4jLogger.php
new file mode 100644
index 00000000..60276411
--- /dev/null
+++ b/src/Common/Neo4jLogger.php
@@ -0,0 +1,71 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Laudis\Neo4j\Common;
+
+use InvalidArgumentException;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
+
+class Neo4jLogger
+{
+ private const LOG_LEVELS = [
+ LogLevel::EMERGENCY,
+ LogLevel::ALERT,
+ LogLevel::CRITICAL,
+ LogLevel::ERROR,
+ LogLevel::WARNING,
+ LogLevel::NOTICE,
+ LogLevel::INFO,
+ LogLevel::DEBUG,
+ ];
+
+ public function __construct(
+ private readonly string $level,
+ private readonly ?LoggerInterface $logger,
+ ) {}
+
+ public function log(string $level, string $message, array $context = []): void
+ {
+ if ($this->logger === null || !$this->shouldLog($level)) {
+ return;
+ }
+
+ match ($level) {
+ LogLevel::EMERGENCY => $this->logger->emergency($message, $context),
+ LogLevel::ALERT => $this->logger->alert($message, $context),
+ LogLevel::CRITICAL => $this->logger->critical($message, $context),
+ LogLevel::ERROR => $this->logger->error($message, $context),
+ LogLevel::WARNING => $this->logger->warning($message, $context),
+ LogLevel::NOTICE => $this->logger->notice($message, $context),
+ LogLevel::INFO => $this->logger->info($message, $context),
+ LogLevel::DEBUG => $this->logger->debug($message, $context),
+ default => throw new InvalidArgumentException("Invalid log level: $level"),
+ };
+ }
+
+ public function getLevel(): string
+ {
+ return $this->level;
+ }
+
+ public function getLogger(): ?LoggerInterface
+ {
+ return $this->logger;
+ }
+
+ private function shouldLog(string $level): bool
+ {
+ return array_search($level, self::LOG_LEVELS) <= array_search($this->level, self::LOG_LEVELS);
+ }
+}
diff --git a/src/Contracts/AuthenticateInterface.php b/src/Contracts/AuthenticateInterface.php
index ba36ca14..1379e545 100644
--- a/src/Contracts/AuthenticateInterface.php
+++ b/src/Contracts/AuthenticateInterface.php
@@ -25,8 +25,6 @@
interface AuthenticateInterface
{
/**
- * @psalm-mutation-free
- *
* Authenticates a RequestInterface with the provided configuration Uri and userAgent.
*/
public function authenticateHttp(RequestInterface $request, UriInterface $uri, string $userAgent): RequestInterface;
diff --git a/src/Contracts/ConnectionInterface.php b/src/Contracts/ConnectionInterface.php
index 3d917d99..e95988b6 100644
--- a/src/Contracts/ConnectionInterface.php
+++ b/src/Contracts/ConnectionInterface.php
@@ -96,8 +96,6 @@ public function setTimeout(float $timeout): void;
/**
* Checks to see if the connection is open.
- *
- * @psalm-mutation-free
*/
public function isOpen(): bool;
@@ -112,4 +110,9 @@ public function getEncryptionLevel(): string;
* Returns the user agent handling this connection.
*/
public function getUserAgent(): string;
+
+ /**
+ * Closes the connection.
+ */
+ public function close(): void;
}
diff --git a/src/Contracts/ConnectionPoolInterface.php b/src/Contracts/ConnectionPoolInterface.php
index 56978b66..98dd372c 100644
--- a/src/Contracts/ConnectionPoolInterface.php
+++ b/src/Contracts/ConnectionPoolInterface.php
@@ -44,4 +44,9 @@ public function acquire(SessionConfiguration $config): Generator;
* Releases a connection back to the pool.
*/
public function release(ConnectionInterface $connection): void;
+
+ /**
+ * Closes all connections in the pool.
+ */
+ public function close(): void;
}
diff --git a/src/Contracts/DriverInterface.php b/src/Contracts/DriverInterface.php
index ea0ca2b3..9d9b0018 100644
--- a/src/Contracts/DriverInterface.php
+++ b/src/Contracts/DriverInterface.php
@@ -38,4 +38,9 @@ public function createSession(?SessionConfiguration $config = null): SessionInte
* Returns true if the driver can make a valid connection with the server.
*/
public function verifyConnectivity(?SessionConfiguration $config = null): bool;
+
+ /**
+ * Closes all connections in the pool.
+ */
+ public function closeConnections(): void;
}
diff --git a/src/Databags/DriverConfiguration.php b/src/Databags/DriverConfiguration.php
index 05b81ddc..18d57613 100644
--- a/src/Databags/DriverConfiguration.php
+++ b/src/Databags/DriverConfiguration.php
@@ -21,8 +21,11 @@
use function is_callable;
use Laudis\Neo4j\Common\Cache;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Common\SemaphoreFactory;
use Laudis\Neo4j\Contracts\SemaphoreFactoryInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
use Psr\SimpleCache\CacheInterface;
use function sprintf;
@@ -42,11 +45,13 @@ final class DriverConfiguration
private $cache;
/** @var callable():(SemaphoreFactoryInterface|null)|SemaphoreFactoryInterface|null */
private $semaphoreFactory;
+ private ?Neo4jLogger $logger;
/**
* @param callable():(HttpPsrBindings|null)|HttpPsrBindings|null $httpPsrBindings
* @param callable():(CacheInterface|null)|CacheInterface|null $cache
* @param callable():(SemaphoreFactoryInterface|null)|SemaphoreFactoryInterface|null $semaphore
+ * @param string|null $logLevel The log level to use. If null, LogLevel::INFO is used.
*
* @psalm-external-mutation-free
*/
@@ -57,11 +62,18 @@ public function __construct(
private int|null $maxPoolSize,
CacheInterface|callable|null $cache,
private float|null $acquireConnectionTimeout,
- callable|SemaphoreFactoryInterface|null $semaphore
+ callable|SemaphoreFactoryInterface|null $semaphore,
+ ?string $logLevel,
+ ?LoggerInterface $logger
) {
$this->httpPsrBindings = $httpPsrBindings;
$this->cache = $cache;
$this->semaphoreFactory = $semaphore;
+ if ($logger !== null) {
+ $this->logger = new Neo4jLogger($logLevel ?? LogLevel::INFO, $logger);
+ } else {
+ $this->logger = null;
+ }
}
/**
@@ -69,9 +81,28 @@ public function __construct(
*
* @pure
*/
- public static function create(?string $userAgent, callable|HttpPsrBindings|null $httpPsrBindings, SslConfiguration $sslConfig, int $maxPoolSize, CacheInterface $cache, float $acquireConnectionTimeout, SemaphoreFactoryInterface $semaphore): self
- {
- return new self($userAgent, $httpPsrBindings, $sslConfig, $maxPoolSize, $cache, $acquireConnectionTimeout, $semaphore);
+ public static function create(
+ ?string $userAgent,
+ callable|HttpPsrBindings|null $httpPsrBindings,
+ SslConfiguration $sslConfig,
+ int $maxPoolSize,
+ CacheInterface $cache,
+ float $acquireConnectionTimeout,
+ SemaphoreFactoryInterface $semaphore,
+ ?string $logLevel,
+ ?LoggerInterface $logger,
+ ): self {
+ return new self(
+ $userAgent,
+ $httpPsrBindings,
+ $sslConfig,
+ $maxPoolSize,
+ $cache,
+ $acquireConnectionTimeout,
+ $semaphore,
+ $logLevel,
+ $logger
+ );
}
/**
@@ -82,7 +113,17 @@ public static function create(?string $userAgent, callable|HttpPsrBindings|null
*/
public static function default(): self
{
- return new self(null, HttpPsrBindings::default(), SslConfiguration::default(), null, null, null, null);
+ return new self(
+ null,
+ HttpPsrBindings::default(),
+ SslConfiguration::default(),
+ null,
+ null,
+ null,
+ null,
+ null,
+ null
+ );
}
/**
@@ -155,7 +196,9 @@ public function getSslConfiguration(): SslConfiguration
public function getHttpPsrBindings(): HttpPsrBindings
{
- $this->httpPsrBindings = (is_callable($this->httpPsrBindings)) ? call_user_func($this->httpPsrBindings) : $this->httpPsrBindings;
+ $this->httpPsrBindings = (is_callable($this->httpPsrBindings)) ? call_user_func(
+ $this->httpPsrBindings
+ ) : $this->httpPsrBindings;
return $this->httpPsrBindings ??= HttpPsrBindings::default();
}
@@ -198,7 +241,9 @@ public function getCache(): CacheInterface
public function getSemaphoreFactory(): SemaphoreFactoryInterface
{
- $this->semaphoreFactory = (is_callable($this->semaphoreFactory)) ? call_user_func($this->semaphoreFactory) : $this->semaphoreFactory;
+ $this->semaphoreFactory = (is_callable($this->semaphoreFactory)) ? call_user_func(
+ $this->semaphoreFactory
+ ) : $this->semaphoreFactory;
return $this->semaphoreFactory ??= SemaphoreFactory::getInstance();
}
@@ -234,4 +279,20 @@ public function withSemaphoreFactory($factory): self
return $tbr;
}
+
+ /**
+ * @psalm-immutable
+ */
+ public function getLogger(): ?Neo4jLogger
+ {
+ return $this->logger;
+ }
+
+ public function withLogger(?string $logLevel, ?LoggerInterface $logger): self
+ {
+ $tbr = clone $this;
+ $tbr->logger = new Neo4jLogger($logLevel ?? LogLevel::INFO, $logger);
+
+ return $tbr;
+ }
}
diff --git a/src/Databags/SessionConfiguration.php b/src/Databags/SessionConfiguration.php
index e0af5938..56adcdc9 100644
--- a/src/Databags/SessionConfiguration.php
+++ b/src/Databags/SessionConfiguration.php
@@ -13,6 +13,7 @@
namespace Laudis\Neo4j\Databags;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Enum\AccessMode;
use function parse_str;
@@ -38,7 +39,8 @@ public function __construct(
private readonly ?string $database = null,
private readonly int|null $fetchSize = null,
private readonly AccessMode|null $accessMode = null,
- private readonly array|null $bookmarks = null
+ private readonly array|null $bookmarks = null,
+ private readonly ?Neo4jLogger $logger = null,
) {}
/**
@@ -46,9 +48,9 @@ public function __construct(
*
* @param list|null $bookmarks
*/
- public static function create(string|null $database = null, int|null $fetchSize = null, AccessMode|null $defaultAccessMode = null, array|null $bookmarks = null): self
+ public static function create(string|null $database = null, int|null $fetchSize = null, AccessMode|null $defaultAccessMode = null, array|null $bookmarks = null, ?Neo4jLogger $logger = null): self
{
- return new self($database, $fetchSize, $defaultAccessMode, $bookmarks);
+ return new self($database, $fetchSize, $defaultAccessMode, $bookmarks, $logger);
}
/**
@@ -64,7 +66,7 @@ public static function default(): self
*/
public function withDatabase(?string $database): self
{
- return new self($database, $this->fetchSize, $this->accessMode, $this->bookmarks);
+ return new self($database, $this->fetchSize, $this->accessMode, $this->bookmarks, $this->logger);
}
/**
@@ -72,7 +74,7 @@ public function withDatabase(?string $database): self
*/
public function withFetchSize(?int $size): self
{
- return new self($this->database, $size, $this->accessMode, $this->bookmarks);
+ return new self($this->database, $size, $this->accessMode, $this->bookmarks, $this->logger);
}
/**
@@ -80,7 +82,7 @@ public function withFetchSize(?int $size): self
*/
public function withAccessMode(?AccessMode $defaultAccessMode): self
{
- return new self($this->database, $this->fetchSize, $defaultAccessMode, $this->bookmarks);
+ return new self($this->database, $this->fetchSize, $defaultAccessMode, $this->bookmarks, $this->logger);
}
/**
@@ -90,7 +92,15 @@ public function withAccessMode(?AccessMode $defaultAccessMode): self
*/
public function withBookmarks(?array $bookmarks): self
{
- return new self($this->database, $this->fetchSize, $this->accessMode, $bookmarks);
+ return new self($this->database, $this->fetchSize, $this->accessMode, $bookmarks, $this->logger);
+ }
+
+ /**
+ * Creates a new session with the provided logger.
+ */
+ public function withLogger(?Neo4jLogger $logger): self
+ {
+ return new self($this->database, $this->fetchSize, $this->accessMode, $this->bookmarks, $logger);
}
/**
@@ -129,6 +139,11 @@ public function getBookmarks(): array
return $this->bookmarks ?? [];
}
+ public function getLogger(): ?Neo4jLogger
+ {
+ return $this->logger;
+ }
+
/**
* Creates a new configuration by merging the provided configuration with the current one.
* The set values of the provided configuration will override the values of this configuration.
@@ -148,7 +163,7 @@ public function merge(SessionConfiguration $config): self
*
* @pure
*/
- public static function fromUri(UriInterface $uri): self
+ public static function fromUri(UriInterface $uri, ?Neo4jLogger $logger): self
{
/**
* @psalm-suppress ImpureMethodCall Uri is a pure object:
@@ -159,6 +174,11 @@ public static function fromUri(UriInterface $uri): self
/** @psalm-suppress ImpureFunctionCall */
parse_str($uri, $query);
$tbr = SessionConfiguration::default();
+
+ if ($logger !== null) {
+ $tbr = $tbr->withLogger($logger);
+ }
+
if (array_key_exists('database', $query)) {
$database = (string) $query['database'];
$tbr = $tbr->withDatabase($database);
diff --git a/src/DriverFactory.php b/src/DriverFactory.php
index 56bca058..fe555b99 100644
--- a/src/DriverFactory.php
+++ b/src/DriverFactory.php
@@ -26,6 +26,7 @@
use Laudis\Neo4j\Http\HttpDriver;
use Laudis\Neo4j\Neo4j\Neo4jDriver;
use Psr\Http\Message\UriInterface;
+use Psr\Log\LoggerInterface;
/**
* Factory for creating drivers directly.
@@ -45,7 +46,7 @@ final class DriverFactory
* : DriverInterface
* )
*/
- public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null, FormatterInterface $formatter = null): DriverInterface
+ public static function create(string|UriInterface $uri, ?DriverConfiguration $configuration = null, ?AuthenticateInterface $authenticate = null, FormatterInterface $formatter = null, ?string $logLevel = null, ?LoggerInterface $logger = null): DriverInterface
{
if (is_string($uri)) {
$uri = Uri::create($uri);
diff --git a/src/Http/HttpConnectionPool.php b/src/Http/HttpConnectionPool.php
index ac2fd8a2..2c0e3929 100644
--- a/src/Http/HttpConnectionPool.php
+++ b/src/Http/HttpConnectionPool.php
@@ -127,4 +127,9 @@ public function release(ConnectionInterface $connection): void
{
// Nothing to release in the current HTTP Protocol implementation
}
+
+ public function close(): void
+ {
+ // Nothing to close in the current HTTP Protocol implementation
+ }
}
diff --git a/src/Http/HttpDriver.php b/src/Http/HttpDriver.php
index 4a03bd5b..76bcfa8d 100644
--- a/src/Http/HttpDriver.php
+++ b/src/Http/HttpDriver.php
@@ -76,20 +76,21 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co
$uri = Uri::create($uri);
}
+ $configuration ??= DriverConfiguration::default();
if ($formatter !== null) {
return new self(
$uri,
- $configuration ?? DriverConfiguration::default(),
+ $configuration,
$formatter,
- $authenticate ?? Authenticate::fromUrl($uri)
+ $authenticate ?? Authenticate::fromUrl($uri, $configuration->getLogger())
);
}
return new self(
$uri,
- $configuration ?? DriverConfiguration::default(),
+ $configuration,
OGMFormatter::create(),
- $authenticate ?? Authenticate::fromUrl($uri)
+ $authenticate ?? Authenticate::fromUrl($uri, $configuration->getLogger())
);
}
@@ -100,7 +101,7 @@ public function createSession(?SessionConfiguration $config = null): SessionInte
{
$factory = $this->resolvableFactory();
$config ??= SessionConfiguration::default();
- $config = $config->merge(SessionConfiguration::fromUri($this->uri));
+ $config = $config->merge(SessionConfiguration::fromUri($this->uri, null));
$streamFactoryResolve = $this->streamFactory();
$tsxUrl = $this->tsxUrl($config);
@@ -197,4 +198,9 @@ private function tsxUrl(SessionConfiguration $config): Resolvable
return str_replace('{databaseName}', $database, $tsx);
});
}
+
+ public function closeConnections(): void
+ {
+ // Nothing to close in the current HTTP Protocol implementation
+ }
}
diff --git a/src/Neo4j/Neo4jConnectionPool.php b/src/Neo4j/Neo4jConnectionPool.php
index db1f3930..75de8213 100644
--- a/src/Neo4j/Neo4jConnectionPool.php
+++ b/src/Neo4j/Neo4jConnectionPool.php
@@ -27,6 +27,7 @@
use Laudis\Neo4j\BoltFactory;
use Laudis\Neo4j\Common\Cache;
use Laudis\Neo4j\Common\GeneratorHelper;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Common\Uri;
use Laudis\Neo4j\Contracts\AddressResolverInterface;
use Laudis\Neo4j\Contracts\AuthenticateInterface;
@@ -40,6 +41,7 @@
use Laudis\Neo4j\Enum\AccessMode;
use Laudis\Neo4j\Enum\RoutingRoles;
use Psr\Http\Message\UriInterface;
+use Psr\Log\LogLevel;
use Psr\SimpleCache\CacheInterface;
use function random_int;
@@ -73,14 +75,20 @@ public function __construct(
private readonly BoltFactory $factory,
private readonly ConnectionRequestData $data,
private readonly CacheInterface $cache,
- private readonly AddressResolverInterface $resolver
+ private readonly AddressResolverInterface $resolver,
+ private readonly ?Neo4jLogger $logger,
) {}
- public static function create(UriInterface $uri, AuthenticateInterface $auth, DriverConfiguration $conf, AddressResolverInterface $resolver, SemaphoreInterface $semaphore): self
- {
+ public static function create(
+ UriInterface $uri,
+ AuthenticateInterface $auth,
+ DriverConfiguration $conf,
+ AddressResolverInterface $resolver,
+ SemaphoreInterface $semaphore
+ ): self {
return new self(
$semaphore,
- BoltFactory::create(),
+ BoltFactory::create($conf->getLogger()),
new ConnectionRequestData(
$uri->getHost(),
$uri,
@@ -89,7 +97,8 @@ public static function create(UriInterface $uri, AuthenticateInterface $auth, Dr
$conf->getSslConfiguration()
),
Cache::getInstance(),
- $resolver
+ $resolver,
+ $conf->getLogger()
);
}
@@ -105,7 +114,7 @@ public function createOrGetPool(string $hostname, UriInterface $uri): Connection
$key = $this->createKey($data);
if (!array_key_exists($key, self::$pools)) {
- self::$pools[$key] = new ConnectionPool($this->semaphore, $this->factory, $data);
+ self::$pools[$key] = new ConnectionPool($this->semaphore, $this->factory, $data, $this->logger);
}
return self::$pools[$key];
@@ -125,14 +134,14 @@ public function acquire(SessionConfiguration $config): Generator
$latestError = null;
if ($table == null) {
- $addresses = (function () {
- yield gethostbyname($this->data->getUri()->getHost());
- yield from $this->resolver->getAddresses($this->data->getUri()->getHost());
- })();
+ $addresses = $this->getAddresses($this->data->getUri()->getHost());
foreach ($addresses as $address) {
$triedAddresses[] = $address;
- $pool = $this->createOrGetPool($this->data->getUri()->getHost(), $this->data->getUri()->withHost($address));
+ $pool = $this->createOrGetPool(
+ $this->data->getUri()->getHost(),
+ $this->data->getUri()->withHost($address)
+ );
try {
/** @var BoltConnection $connection */
$connection = GeneratorHelper::getReturnFromGenerator($pool->acquire($config));
@@ -144,6 +153,7 @@ public function acquire(SessionConfiguration $config): Generator
}
$this->cache->set($key, $table, $table->getTtl());
+ // TODO: release probably logs off the connection, it is not preferable
$pool->release($connection);
break;
@@ -163,6 +173,11 @@ public function acquire(SessionConfiguration $config): Generator
return $this->createOrGetPool($this->data->getUri()->getHost(), $server)->acquire($config);
}
+ public function getLogger(): ?Neo4jLogger
+ {
+ return $this->logger;
+ }
+
/**
* @throws Exception
*/
@@ -187,8 +202,9 @@ private function getNextServer(RoutingTable $table, AccessMode $mode): ?Uri
*/
private function routingTable(BoltConnection $connection, SessionConfiguration $config): RoutingTable
{
- $bolt = $connection->getImplementation()[0];
+ $bolt = $connection->protocol();
+ $this->getLogger()?->log(LogLevel::DEBUG, 'ROUTE', ['db' => $config->getDatabase()]);
/** @var array{rt: array{servers: list, role:string}>, ttl: int}} $route */
$route = $bolt->route([], [], ['db' => $config->getDatabase()])
->getResponse()
@@ -202,7 +218,9 @@ private function routingTable(BoltConnection $connection, SessionConfiguration $
public function release(ConnectionInterface $connection): void
{
- $this->createOrGetPool($connection->getServerAddress()->getHost(), $connection->getServerAddress())->release($connection);
+ $this->createOrGetPool($connection->getServerAddress()->getHost(), $connection->getServerAddress())->release(
+ $connection
+ );
}
private function createKey(ConnectionRequestData $data, ?SessionConfiguration $config = null): string
@@ -211,7 +229,14 @@ private function createKey(ConnectionRequestData $data, ?SessionConfiguration $c
$key = implode(
':',
- array_filter([$data->getUserAgent(), $uri->getHost(), $config ? $config->getDatabase() : null, $uri->getPort() ?? '7687'])
+ array_filter(
+ [
+ $data->getUserAgent(),
+ $uri->getHost(),
+ $config ? $config->getDatabase() : null,
+ $uri->getPort() ?? '7687',
+ ]
+ )
);
return str_replace([
@@ -225,4 +250,22 @@ private function createKey(ConnectionRequestData $data, ?SessionConfiguration $c
':',
], '|', $key);
}
+
+ public function close(): void
+ {
+ foreach (self::$pools as $pool) {
+ $pool->close();
+ }
+ self::$pools = [];
+ $this->cache->clear();
+ }
+
+ /**
+ * @return Generator
+ */
+ private function getAddresses(string $host): Generator
+ {
+ yield gethostbyname($host);
+ yield from $this->resolver->getAddresses($host);
+ }
}
diff --git a/src/Neo4j/Neo4jDriver.php b/src/Neo4j/Neo4jDriver.php
index 24cf9f71..bc2a1695 100644
--- a/src/Neo4j/Neo4jDriver.php
+++ b/src/Neo4j/Neo4jDriver.php
@@ -75,7 +75,7 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co
}
$configuration ??= DriverConfiguration::default();
- $authenticate ??= Authenticate::fromUrl($uri);
+ $authenticate ??= Authenticate::fromUrl($uri, $configuration->getLogger());
$resolver ??= new DNSAddressResolver();
$semaphore = $configuration->getSemaphoreFactory()->create($uri, $configuration);
@@ -95,7 +95,7 @@ public static function create(string|UriInterface $uri, ?DriverConfiguration $co
public function createSession(?SessionConfiguration $config = null): SessionInterface
{
$config ??= SessionConfiguration::default();
- $config = $config->merge(SessionConfiguration::fromUri($this->parsedUrl));
+ $config = $config->merge(SessionConfiguration::fromUri($this->parsedUrl, $this->pool->getLogger()));
return new Session($config, $this->pool, $this->formatter);
}
@@ -111,4 +111,9 @@ public function verifyConnectivity(?SessionConfiguration $config = null): bool
return true;
}
+
+ public function closeConnections(): void
+ {
+ $this->pool->close();
+ }
}
diff --git a/tests/EnvironmentAwareIntegrationTest.php b/tests/EnvironmentAwareIntegrationTest.php
index d84375ad..f71408f9 100644
--- a/tests/EnvironmentAwareIntegrationTest.php
+++ b/tests/EnvironmentAwareIntegrationTest.php
@@ -17,27 +17,40 @@
use Laudis\Neo4j\Basic\Driver;
use Laudis\Neo4j\Basic\Session;
+use Laudis\Neo4j\Common\Neo4jLogger;
use Laudis\Neo4j\Common\Uri;
+use Laudis\Neo4j\Databags\DriverConfiguration;
use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Psr\Log\LogLevel;
+use RuntimeException;
abstract class EnvironmentAwareIntegrationTest extends TestCase
{
- protected static Session $session;
- protected static Driver $driver;
- protected static Uri $uri;
+ protected Session $session;
+ protected Driver $driver;
+ protected Uri $uri;
+ protected Neo4jLogger $logger;
- public static function setUpBeforeClass(): void
+ public function setUp(): void
{
- parent::setUpBeforeClass();
+ parent::setUp();
$connection = $_ENV['CONNECTION'] ?? false;
if (!is_string($connection)) {
$connection = 'bolt://localhost';
}
- self::$uri = Uri::create($connection);
- self::$driver = Driver::create(self::$uri);
- self::$session = self::$driver->createSession();
+ /** @noinspection PhpUnhandledExceptionInspection */
+ $conf = DriverConfiguration::default()->withLogger(LogLevel::DEBUG, $this->createMock(LoggerInterface::class));
+ $logger = $conf->getLogger();
+ if ($logger === null) {
+ throw new RuntimeException('Logger not set');
+ }
+ $this->logger = $logger;
+ $this->uri = Uri::create($connection);
+ $this->driver = Driver::create($this->uri, $conf);
+ $this->session = $this->driver->createSession();
}
/**
@@ -47,7 +60,7 @@ public function getSession(array|string|null $forceScheme = null): Session
{
$this->skipUnsupportedScheme($forceScheme);
- return self::$session;
+ return $this->session;
}
/**
@@ -57,7 +70,7 @@ public function getUri(array|string|null $forceScheme = null): Uri
{
$this->skipUnsupportedScheme($forceScheme);
- return self::$uri;
+ return $this->uri;
}
/**
@@ -80,7 +93,7 @@ private function skipUnsupportedScheme(array|string|null $forceScheme): void
$options[] = $scheme.'+ssc';
}
- if (!in_array(self::$uri->getScheme(), $options)) {
+ if (!in_array($this->uri->getScheme(), $options)) {
/** @psalm-suppress MixedArgumentTypeCoercion */
$this->markTestSkipped(sprintf(
'Connection only for types: "%s"',
@@ -96,6 +109,11 @@ protected function getDriver(array|string|null $forceScheme = null): Driver
{
$this->skipUnsupportedScheme($forceScheme);
- return self::$driver;
+ return $this->driver;
+ }
+
+ protected function getNeo4jLogger(): Neo4jLogger
+ {
+ return $this->logger;
}
}
diff --git a/tests/Integration/BoltResultIntegrationTest.php b/tests/Integration/BoltResultIntegrationTest.php
index 9db763a7..4868aace 100644
--- a/tests/Integration/BoltResultIntegrationTest.php
+++ b/tests/Integration/BoltResultIntegrationTest.php
@@ -44,7 +44,7 @@ public function testIterationLong(): void
SessionConfiguration::default()
);
- $connection->getImplementation()[0]->run('UNWIND range(1, 100000) AS i RETURN i')
+ $connection->protocol()->run('UNWIND range(1, 100000) AS i RETURN i')
->getResponse();
$result = new BoltResult($connection, 1000, -1);
foreach ($result as $i => $x) {
diff --git a/tests/Integration/EdgeCasesTest.php b/tests/Integration/EdgeCasesTest.php
index cc61895e..cc19152b 100644
--- a/tests/Integration/EdgeCasesTest.php
+++ b/tests/Integration/EdgeCasesTest.php
@@ -24,15 +24,15 @@
final class EdgeCasesTest extends EnvironmentAwareIntegrationTest
{
- public static function setUpBeforeClass(): void
+ public function setUp(): void
{
- parent::setUpBeforeClass();
- self::$session->run(MoviesFixture::CQL);
+ parent::setUp();
+ $this->session->run(MoviesFixture::CQL);
}
public function testCanHandleMapLiterals(): void
{
- $results = self::$session->run('MATCH (n:Person)-[r:ACTED_IN]->(m) RETURN n, {movie: m, roles: r.roles} AS actInfo LIMIT 5');
+ $results = $this->session->run('MATCH (n:Person)-[r:ACTED_IN]->(m) RETURN n, {movie: m, roles: r.roles} AS actInfo LIMIT 5');
foreach ($results as $result) {
$actorInfo = $result->get('actInfo');
diff --git a/tests/Integration/Neo4jLoggerTest.php b/tests/Integration/Neo4jLoggerTest.php
new file mode 100644
index 00000000..40f9bd06
--- /dev/null
+++ b/tests/Integration/Neo4jLoggerTest.php
@@ -0,0 +1,145 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Laudis\Neo4j\Tests\Integration;
+
+use Laudis\Neo4j\Bolt\Session;
+use Laudis\Neo4j\Databags\SessionConfiguration;
+use Laudis\Neo4j\Databags\Statement;
+use Laudis\Neo4j\Databags\TransactionConfiguration;
+use Laudis\Neo4j\Tests\EnvironmentAwareIntegrationTest;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class Neo4jLoggerTest extends EnvironmentAwareIntegrationTest
+{
+ public function testLogger(): void
+ {
+ if ($this->getUri()->getScheme() === 'http') {
+ self::markTestSkipped('This test is not applicable for the HTTP driver');
+ }
+
+ // Close connections so that we can test the logger logging
+ // during authentication while acquiring a new connection
+ $this->driver->closeConnections();
+
+ /** @var MockObject $logger */
+ $logger = $this->getNeo4jLogger()->getLogger();
+ /** @var Session $session */
+ $session = $this->getSession();
+
+ /** @var array $infoLogs */
+ $infoLogs = [];
+ $expectedInfoLogs = [
+ [
+ 'Running statements',
+ [
+ 'statements' => [new Statement('RETURN 1 as test', [])],
+ ],
+ ],
+ [
+ 'Starting instant transaction',
+ [
+ 'config' => new TransactionConfiguration(null, null),
+ ],
+ ],
+ [
+ 'Acquiring connection',
+ [
+ 'config' => new TransactionConfiguration(null, null),
+ ],
+ ],
+ ];
+ $logger->expects(self::exactly(count($expectedInfoLogs)))->method('info')->willReturnCallback(
+ static function (string $message, array $context) use (&$infoLogs) {
+ $infoLogs[] = [$message, $context];
+ }
+ );
+
+ $debugLogs = [];
+ $expectedDebugLogs = [
+ [
+ 'HELLO',
+ [
+ 'user_agent' => 'neo4j-php-client/2',
+ ],
+ ],
+ [
+ 'LOGON',
+ [
+ 'scheme' => 'basic',
+ 'principal' => 'neo4j',
+ ],
+ ],
+ [
+ 'RUN',
+ [
+ 'mode' => 'w',
+ ],
+ ],
+ [
+ 'DISCARD',
+ [],
+ ],
+ ];
+
+ if ($this->getUri()->getScheme() === 'neo4j') {
+ array_splice(
+ $expectedDebugLogs,
+ 0,
+ 0,
+ [
+ [
+ 'HELLO',
+ [
+ 'user_agent' => 'neo4j-php-client/2',
+ ],
+ ],
+ [
+ 'LOGON',
+ [
+ 'scheme' => 'basic',
+ 'principal' => 'neo4j',
+ ],
+ ],
+ [
+ 'ROUTE',
+ [
+ 'db' => null,
+ ],
+ ],
+ [
+ 'GOODBYE',
+ [],
+ ],
+ ],
+ );
+ }
+
+ $logger->expects(self::exactly(count($expectedDebugLogs)))->method('debug')->willReturnCallback(
+ static function (string $message, array $context) use (&$debugLogs) {
+ $debugLogs[] = [$message, $context];
+ }
+ );
+
+ $session->run('RETURN 1 as test');
+
+ self::assertCount(3, $infoLogs);
+ self::assertEquals(array_slice($expectedInfoLogs, 0, 2), array_slice($infoLogs, 0, 2));
+ /** @psalm-suppress PossiblyUndefinedIntArrayOffset */
+ self::assertEquals($expectedInfoLogs[2][0], $infoLogs[2][0]);
+ /** @psalm-suppress PossiblyUndefinedIntArrayOffset */
+ self::assertInstanceOf(SessionConfiguration::class, $infoLogs[2][1]['sessionConfig']);
+
+ self::assertEquals($expectedDebugLogs, $debugLogs);
+ }
+}
diff --git a/tests/Unit/BoltConnectionPoolTest.php b/tests/Unit/BoltConnectionPoolTest.php
index bf717c61..3c77634a 100644
--- a/tests/Unit/BoltConnectionPoolTest.php
+++ b/tests/Unit/BoltConnectionPoolTest.php
@@ -156,13 +156,16 @@ private function setupPool(Generator $semaphoreGenerator): void
->willReturnCallback(fn (MockObject $x): MockObject => $x);
$this->pool = new ConnectionPool(
- $this->semaphore, $this->factory, new ConnectionRequestData(
+ $this->semaphore,
+ $this->factory,
+ new ConnectionRequestData(
'',
Uri::create(''),
Authenticate::disabled(),
'',
SslConfiguration::default()
- )
+ ),
+ null,
);
}
}