Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Pure non-blocking (fiber based) strictly typed full-featured PHP driver for NATS

## Features
- [NATS Core](https://docs.nats.io/nats-concepts/core-nats)
- [Connection](#connection)
- [Publish-Subscribe](#pub-sub)
- [Queues](#queues)
- [Request-Reply](#request-reply)
Expand Down Expand Up @@ -58,6 +59,28 @@ composer require thesis/nats

The library implements the full functionality of NATS Core, including pub-sub, queues and request–reply.

#### Connection

`Nats\Config::fromURI()` takes `[scheme://][user[:password]@]host[:port][,host[:port]...][?options]`. The scheme is `nats://` (`tcp://` is an alias, and it may be omitted),
`tls://`, `ws://` and `wss://` are rejected as not supported yet. A host without a port connects to `4222`, an IPv6 host goes in brackets (`[::1]:4222`),
and the credentials are percent-encoded like in any url (`p%40ss` for `p@ss`). The servers of a list are tried in order until one accepts the connection;
each one may repeat the scheme and the credentials, as in nats.go (`nats://u:p@h1:4222,nats://u:p@h2:4222`): one user is sent to every server, so different credentials are rejected. The options are the snake_case names of the `Nats\Config` parameters,
e.g. `connection_timeout` (seconds, fractions allowed), `request_timeout`, `verbose`, `no_responders`, `ping`, `jwt`, `nkey`, `client_name`:

```php
<?php

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

use Thesis\Nats;

$nc = new Nats\Client(Nats\Config::fromURI('nats://user:p%40ss@nats-1:4222,nats-2,[::1]:4223?connection_timeout=2.5&no_responders=true'));

dump($nc->jetStream()->accountInfo());
```

#### Pub-Sub

```php
Expand Down
142 changes: 26 additions & 116 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Thesis\Nats;

use Thesis\Nats\Internal\Connection\Uri;
use Thesis\Nats\Internal\Validation\Validate;

/**
Expand All @@ -12,9 +13,7 @@
final readonly class Config
{
private const string DEFAULT_URL = '127.0.0.1:4222';
private const string DEFAULT_HOST = '127.0.0.1';
private const int DEFAULT_PORT = 4222;
private const int DEFAULT_CONNECTION_TIMEOUT = 10;
private const float DEFAULT_CONNECTION_TIMEOUT = 10.0;
private const int DEFAULT_PING_INTERVAL = 10000;
private const int DEFAULT_MAX_PINGS = 5;
private const string DEFAULT_CLIENT_NAME = 'thesis/nats';
Expand Down Expand Up @@ -60,6 +59,8 @@ public function __construct(
public string $clientName = self::DEFAULT_CLIENT_NAME,
public ?float $requestTimeout = null,
) {
Validate::duration($connectionTimeout, 'connection timeout');

if ($requestTimeout !== null) {
Validate::duration($requestTimeout, 'request timeout');
}
Expand All @@ -78,120 +79,29 @@ public static function default(): self
*/
public static function fromURI(#[\SensitiveParameter] string $uri): self
{
$components = parse_url($uri);

if ($components === false) {
throw new \InvalidArgumentException("The uri '{$uri}' is invalid.");
}

$query = [];
if (isset($components['query']) && $components['query'] !== '') {
parse_str($components['query'], $query);
}

$connectionTimeout = self::DEFAULT_CONNECTION_TIMEOUT;
if (isset($query['connection_timeout']) && is_numeric($query['connection_timeout']) && (int) $query['connection_timeout'] > 0) {
/** @var positive-int $connectionTimeout */
$connectionTimeout = (int) $query['connection_timeout'];
}

$tcpNoDelay = true;
if (isset($query['tcp_nodelay'])) {
$tcpNoDelay = filter_var($query['tcp_nodelay'], FILTER_VALIDATE_BOOL);
}

$verbose = false;
if (isset($query['verbose'])) {
$verbose = filter_var($query['verbose'], FILTER_VALIDATE_BOOL);
}

$pedantic = false;
if (isset($query['pedantic'])) {
$pedantic = filter_var($query['pedantic'], FILTER_VALIDATE_BOOL);
}

$noResponders = false;
if (isset($query['no_responders'])) {
$noResponders = filter_var($query['no_responders'], FILTER_VALIDATE_BOOL);
}

$ping = self::DEFAULT_PING_INTERVAL;
if (isset($query['ping']) && is_numeric($query['ping'])) {
/** @var ?positive-int $ping */
$ping = match ($interval = (int) $query['ping']) {
-1 => null,
default => $interval < 0 ? self::DEFAULT_PING_INTERVAL : $interval,
};
}

$maxPings = self::DEFAULT_MAX_PINGS;
if (isset($query['max_pings']) && is_numeric($query['max_pings']) && (int) $query['max_pings'] > 0) {
/** @var positive-int $maxPings */
$maxPings = (int) $query['max_pings'];
}

$jetStreamDomain = null;
if (isset($query['jetstream_domain']) && \is_string($query['jetstream_domain']) && $query['jetstream_domain'] !== '') {
$jetStreamDomain = $query['jetstream_domain'];
}

$port = self::DEFAULT_PORT;
if (isset($components['port']) && $components['port'] > 0) {
$port = $components['port'];
}

$urls = [];
foreach (explode(',', $components['host'] ?? '') as $host) {
$hostport = explode(':', $host);
$urls[] = \sprintf('%s:%d', $hostport[0] === '' ? self::DEFAULT_HOST : $hostport[0], (int) ($hostport[1] ?? $port));
}

$user = null;
if (isset($components['user']) && $components['user'] !== '') {
$user = $components['user'];
}

$password = null;
if (isset($components['pass']) && $components['pass'] !== '') {
$password = $components['pass'];
}

$jwt = null;
if (isset($query['jwt']) && \is_string($query['jwt']) && $query['jwt'] !== '') {
$jwt = $query['jwt'];
}

$nkey = null;
if (isset($query['nkey']) && \is_string($query['nkey']) && $query['nkey'] !== '') {
$nkey = $query['nkey'];
}

$clientName = self::DEFAULT_CLIENT_NAME;
if (isset($query['client_name']) && \is_string($query['client_name']) && $query['client_name'] !== '') {
$clientName = $query['client_name'];
}

$requestTimeout = null;
if (isset($query['request_timeout']) && is_numeric($query['request_timeout']) && (float) $query['request_timeout'] > 0) {
$requestTimeout = (float) $query['request_timeout'];
}
$parsed = Uri::parse($uri);
$query = $parsed->query;

return new self(
urls: $urls,
verbose: $verbose,
pedantic: $pedantic,
connectionTimeout: $connectionTimeout,
user: $user,
password: $password,
jwt: $jwt,
nkey: $nkey,
tcpNoDelay: $tcpNoDelay,
noResponders: $noResponders,
ping: $ping,
maxPings: $maxPings,
jetStreamDomain: $jetStreamDomain,
clientName: $clientName,
requestTimeout: $requestTimeout,
urls: $parsed->urls,
verbose: $query->verbose ?? false,
pedantic: $query->pedantic ?? false,
connectionTimeout: $query->connectionTimeout ?? self::DEFAULT_CONNECTION_TIMEOUT,
user: $parsed->user,
password: $parsed->password,
jwt: $query->jwt,
nkey: $query->nkey,
tcpNoDelay: $query->tcpNoDelay ?? true,
noResponders: $query->noResponders ?? false,
ping: match (true) {
$query->ping === -1 => null,
$query->ping !== null && $query->ping > 0 => $query->ping,
default => self::DEFAULT_PING_INTERVAL,
},
maxPings: $query->maxPings ?? self::DEFAULT_MAX_PINGS,
jetStreamDomain: $query->jetStreamDomain,
clientName: $query->clientName ?? self::DEFAULT_CLIENT_NAME,
requestTimeout: $query->requestTimeout,
);
}

Expand All @@ -204,7 +114,7 @@ public static function fromURI(#[\SensitiveParameter] string $uri): self
* nkey?: non-empty-string,
* verbose?: bool,
* pedantic?: bool,
* connection_timeout?: positive-int,
* connection_timeout?: float,
* tcp_nodelay?: bool,
* no_responders?: bool,
* ping?: positive-int,
Expand Down
123 changes: 123 additions & 0 deletions src/Internal/Connection/Query.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

declare(strict_types=1);

namespace Thesis\Nats\Internal\Connection;

/**
* The options of a connection uri. An option that is absent or has a value of the wrong type is null,
* so the default of the config applies.
*
* @internal
*/
final readonly class Query
{
/**
* @param ?float $connectionTimeout positive and finite
* @param ?float $requestTimeout positive and finite
* @param ?int $ping any integer, the config decides what a negative one means
* @param ?positive-int $maxPings
* @param ?non-empty-string $jetStreamDomain
* @param ?non-empty-string $clientName
* @param ?non-empty-string $jwt
* @param ?non-empty-string $nkey
*/
private function __construct(
public ?bool $verbose = null,
public ?bool $pedantic = null,
public ?bool $tcpNoDelay = null,
public ?bool $noResponders = null,
public ?float $connectionTimeout = null,
public ?float $requestTimeout = null,
public ?int $ping = null,
public ?int $maxPings = null,
public ?string $jetStreamDomain = null,
public ?string $clientName = null,
#[\SensitiveParameter]
public ?string $jwt = null,
#[\SensitiveParameter]
public ?string $nkey = null,
) {}

public static function parse(#[\SensitiveParameter] string $query): self
{
parse_str($query, $options);

return new self(
verbose: self::bool($options, 'verbose'),
pedantic: self::bool($options, 'pedantic'),
tcpNoDelay: self::bool($options, 'tcp_nodelay'),
noResponders: self::bool($options, 'no_responders'),
connectionTimeout: self::duration($options, 'connection_timeout'),
requestTimeout: self::duration($options, 'request_timeout'),
ping: self::int($options, 'ping'),
maxPings: self::positiveInt($options, 'max_pings'),
jetStreamDomain: self::string($options, 'jetstream_domain'),
clientName: self::string($options, 'client_name'),
jwt: self::string($options, 'jwt'),
nkey: self::string($options, 'nkey'),
);
}

/**
* @param array<array-key, mixed> $options
*/
private static function bool(array $options, string $name): ?bool
{
if (!isset($options[$name])) {
return null;
}

return filter_var($options[$name], FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE);
}

/**
* @param array<array-key, mixed> $options
*/
private static function int(array $options, string $name): ?int
{
if (!isset($options[$name])) {
return null;
}

$value = filter_var($options[$name], FILTER_VALIDATE_INT);

return $value !== false ? $value : null;
}

/**
* @param array<array-key, mixed> $options
* @return ?positive-int
*/
private static function positiveInt(array $options, string $name): ?int
{
$value = self::int($options, $name);

return $value !== null && $value > 0 ? $value : null;
}

/**
* @param array<array-key, mixed> $options
*/
private static function duration(array $options, string $name): ?float
{
if (!isset($options[$name])) {
return null;
}

$value = filter_var($options[$name], FILTER_VALIDATE_FLOAT);

return $value !== false && is_finite($value) && $value > 0 ? $value : null;
}

/**
* @param array<array-key, mixed> $options
* @return ?non-empty-string
*/
private static function string(#[\SensitiveParameter] array $options, string $name): ?string
{
$value = $options[$name] ?? null;

return \is_string($value) && $value !== '' ? $value : null;
}
}
Loading
Loading