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
50 changes: 50 additions & 0 deletions documentation/components/bridges/symfony-postgresql-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,56 @@ flow_postgresql:
dsn: '%env(DATABASE_URL)%'
```

### Connection Overrides

Each connection may override individual parts of the parsed DSN. Every key maps 1:1 onto an immutable
`ConnectionParameters` method in the `flow-php/postgresql` library; the bundle adds no logic of its own.
Overrides are applied at service-factory time, so values may be `%env(...)%` placeholders — they are
resolved at runtime, not when the configuration is parsed.

```yaml
flow_postgresql:
connections:
default:
dsn: '%env(DATABASE_URL)%'

dbname: null # Replaces the database name parsed from the DSN
host: null # Replaces the host parsed from the DSN
port: null # Replaces the port parsed from the DSN
user: null # Replaces the user parsed from the DSN
password: null # Replaces the password parsed from the DSN
dbname_suffix: '' # Appends the given suffix to the configured database name
```

| Key | Type | Default | Effect |
|-----------------|--------|---------|-----------------------------------------------------------|
| `dbname` | string | `null` | Replaces the database name parsed from the DSN. |
| `host` | string | `null` | Replaces the host parsed from the DSN. |
| `port` | int | `null` | Replaces the port parsed from the DSN. |
| `user` | string | `null` | Replaces the user parsed from the DSN. |
| `password` | string | `null` | Replaces the password parsed from the DSN. |
| `dbname_suffix` | string | `''` | Appends the given suffix to the configured database name. |

`dbname`/`host`/`port`/`user`/`password` are applied first, then `dbname_suffix` **last** — so
`dbname: 'foo'` + `dbname_suffix: '_test'` yields `foo_test`. A connection with only `dsn` behaves
exactly as before.

#### `dbname_suffix` for parallel tests

```yaml
# config/packages/flow_postgresql.yaml
flow_postgresql:
connections:
default:
dsn: '%env(DATABASE_ANALYTICAL_URL)%'

when@test:
flow_postgresql:
connections:
default:
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
```

### Telemetry

Enable telemetry per connection to get distributed tracing, query logging, and metrics.
Expand Down
38 changes: 38 additions & 0 deletions documentation/components/libs/postgresql/client-connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,44 @@ $client = pgsql_client(
);
```

## Overriding Parsed Parameters

`ConnectionParameters` is immutable. Each `with*()` method returns a new instance, so a parsed DSN
can be adjusted without re-parsing.

```php
<?php

use Flow\PostgreSql\Client\DsnParser;

$params = (new DsnParser())->parse('postgresql://user:pass@localhost:5432/mydb')
->withHost('db.internal')
->withPort(5544)
->withUser('svc')
->withPassword('secret')
->withDatabase('analytics');
```

### Database Suffix

`withDatabaseSuffix()` appends a suffix to the configured database name. The base DSN stays constant
and only the suffix changes per environment, which makes per-worker parallel-test databases work
(`mydb` → `mydb_test7`) without a second DSN. The suffix is appended verbatim — nothing is inserted
automatically, so include a separator yourself if you want one (e.g. `_test7`). An empty suffix is a
no-op:

```php
<?php

use Flow\PostgreSql\Client\DsnParser;

$params = (new DsnParser())->parse('postgresql://user:pass@localhost:5432/mydb')
->withDatabaseSuffix('_test7'); // database() === 'mydb_test7'
```

When combined with `withDatabase()`, apply the override first — the suffix is appended to the
overridden name (`withDatabase('other')->withDatabaseSuffix('_test')` ⇒ `other_test`).

## Connection Lifecycle

### Checking Connection Status
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Flow\Bridge\Symfony\PostgreSqlBundle\Connection;

use Flow\PostgreSql\Client\ConnectionParameters;
use Flow\PostgreSql\Client\DsnParser;

final class ConnectionParametersFactory
{
/**
* @param array{dbname?: ?string, host?: ?string, port?: ?int, user?: ?string, password?: ?string, dbname_suffix?: string} $overrides
*/
public static function create(DsnParser $parser, string $dsn, array $overrides = []): ConnectionParameters
{
$params = $parser->parse($dsn);

if (($overrides['dbname'] ?? null) !== null) {
$params = $params->withDatabase($overrides['dbname']);
}

if (($overrides['host'] ?? null) !== null) {
$params = $params->withHost($overrides['host']);
}

if (($overrides['port'] ?? null) !== null) {
$params = $params->withPort($overrides['port']);
}

if (($overrides['user'] ?? null) !== null) {
$params = $params->withUser($overrides['user']);
}

if (($overrides['password'] ?? null) !== null) {
$params = $params->withPassword($overrides['password']);
}

return $params->withDatabaseSuffix($overrides['dbname_suffix'] ?? '');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Flow\Bridge\Symfony\PostgreSqlBundle\Attribute\AsCatalogProvider;
use Flow\Bridge\Symfony\PostgreSqlBundle\CatalogProvider\ArrayCatalogProvider;
use Flow\Bridge\Symfony\PostgreSqlBundle\Command\SessionPurgeCommand;
use Flow\Bridge\Symfony\PostgreSqlBundle\Connection\ConnectionParametersFactory;
use Flow\Bridge\Symfony\PostgreSqlBundle\DependencyInjection\Compiler\CatalogProviderPass;
use Flow\Bridge\Symfony\PostgreSqlBundle\DependencyInjection\Compiler\CommandLocatorPass;
use Flow\Bridge\Symfony\PostgreSqlBundle\Generator\TwigMigrationGenerator;
Expand Down Expand Up @@ -107,6 +108,30 @@ public function configure(DefinitionConfigurator $definition): void
->cannotBeEmpty()
->info('PostgreSQL connection DSN (e.g. postgresql://user:pass@localhost:5432/dbname)')
->end()
->scalarNode('dbname')
->defaultNull()
->info('Overrides the database name parsed from the DSN.')
->end()
->scalarNode('host')
->defaultNull()
->info('Overrides the host parsed from the DSN.')
->end()
->integerNode('port')
->defaultNull()
->info('Overrides the port parsed from the DSN.')
->end()
->scalarNode('user')
->defaultNull()
->info('Overrides the user parsed from the DSN.')
->end()
->scalarNode('password')
->defaultNull()
->info('Overrides the password parsed from the DSN.')
->end()
->scalarNode('dbname_suffix')
->defaultValue('')
->info('Adds the given suffix to the configured database name.')
->end()
->booleanNode('test_transaction_rollback')
->defaultFalse()
->info(
Expand Down Expand Up @@ -399,7 +424,7 @@ public function configure(DefinitionConfigurator $definition): void
}

/**
* @param array{connections: array<string, array{dsn: string, test_transaction_rollback: bool, context?: array<string, mixed>, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}}>, messenger: array{enabled: bool, table_name: string, schema: string}, cache: array{pools?: array<string, array{connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, namespace: string, default_lifetime: int, marshaller_service_id: ?string, share_connection: bool}>}, session: array{enabled: bool, connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, lock_mode: string, ttl: ?int, share_connection: bool}, migrations: array{enabled: bool, directory: string, namespace: string, table_name: string, table_schema: string, migration_file_name: string, rollback_file_name: string, all_or_nothing: bool, generate_rollback: bool, exclude?: list<array{schema: ?string, table: ?string, exact: ?string, starts_with: ?string, ends_with: ?string, pattern: ?string, policy_id: ?string, type: ?string, for_schema: ?string}>}, catalog_providers: list<array{catalog_provider_id: ?string, catalog: ?array<string, mixed>}>} $config
* @param array{connections: array<string, array{dsn: string, dbname: ?string, host: ?string, port: ?int, user: ?string, password: ?string, dbname_suffix: string, test_transaction_rollback: bool, context?: array<string, mixed>, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}}>, messenger: array{enabled: bool, table_name: string, schema: string}, cache: array{pools?: array<string, array{connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, namespace: string, default_lifetime: int, marshaller_service_id: ?string, share_connection: bool}>}, session: array{enabled: bool, connection: ?string, table_name: string, schema: string, id_col: string, data_col: string, lifetime_col: string, time_col: string, lock_mode: string, ttl: ?int, share_connection: bool}, migrations: array{enabled: bool, directory: string, namespace: string, table_name: string, table_schema: string, migration_file_name: string, rollback_file_name: string, all_or_nothing: bool, generate_rollback: bool, exclude?: list<array{schema: ?string, table: ?string, exact: ?string, starts_with: ?string, ends_with: ?string, pattern: ?string, policy_id: ?string, type: ?string, for_schema: ?string}>}, catalog_providers: list<array{catalog_provider_id: ?string, catalog: ?array<string, mixed>}>} $config
*/
#[Override]
public function loadExtension(array $config, ContainerConfigurator $configurator, ContainerBuilder $container): void
Expand Down Expand Up @@ -537,7 +562,7 @@ private function registerCatalogProviders(array $catalogProviders, ContainerBuil
}

/**
* @param array{dsn: string, test_transaction_rollback: bool, context?: array<string, mixed>, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}} $connectionConfig
* @param array{dsn: string, dbname: ?string, host: ?string, port: ?int, user: ?string, password: ?string, dbname_suffix: string, test_transaction_rollback: bool, context?: array<string, mixed>, telemetry?: array{service_id: string, clock_service_id: ?string, trace_queries: bool, trace_transactions: bool, collect_metrics: bool, log_queries: bool, max_query_length: int, include_parameters: bool, max_parameters: int, max_parameter_length: int}} $connectionConfig
*/
private function registerConnection(
string $name,
Expand All @@ -549,8 +574,19 @@ private function registerConnection(
$container->setDefinition("flow.postgresql.{$name}.dsn_parser", $parserDef);

$paramsDef = new Definition(ConnectionParameters::class);
$paramsDef->setFactory([new Reference("flow.postgresql.{$name}.dsn_parser"), 'parse']);
$paramsDef->setArguments([$connectionConfig['dsn']]);
$paramsDef->setFactory([ConnectionParametersFactory::class, 'create']);
$paramsDef->setArguments([
new Reference("flow.postgresql.{$name}.dsn_parser"),
$connectionConfig['dsn'],
[
'dbname' => $connectionConfig['dbname'] ?? null,
'host' => $connectionConfig['host'] ?? null,
'port' => $connectionConfig['port'] ?? null,
'user' => $connectionConfig['user'] ?? null,
'password' => $connectionConfig['password'] ?? null,
'dbname_suffix' => $connectionConfig['dbname_suffix'] ?? '',
],
]);
$container->setDefinition("flow.postgresql.{$name}.connection_parameters", $paramsDef);

$paramsDef->setPublic(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,117 @@ public function test_client_without_telemetry_is_not_decorated(): void
static::assertFalse($this->getContainer()->has('flow.postgresql.default.telemetry.config'));
}

public function test_connection_only_dsn_behaves_as_before(): void
{
$this->bootKernel([
'config' => static function (TestKernel $kernel): void {
$kernel->addTestExtensionConfig('flow_postgresql', [
'connections' => [
'default' => [
'dsn' => 'postgresql://postgres:postgres@localhost:5432/app',
],
],
]);
},
]);

$params = $this->getContainer()->get('flow.postgresql.default.connection_parameters');
static::assertSame('app', $params->database());
static::assertSame('localhost', $params->host());
}

public function test_connection_parameters_apply_dbname_override_before_suffix(): void
{
$this->bootKernel([
'config' => static function (TestKernel $kernel): void {
$kernel->addTestExtensionConfig('flow_postgresql', [
'connections' => [
'default' => [
'dsn' => 'postgresql://postgres:postgres@localhost:5432/app',
'dbname' => 'other',
'dbname_suffix' => '_test',
],
],
]);
},
]);

static::assertSame(
'other_test',
$this->getContainer()->get('flow.postgresql.default.connection_parameters')->database(),
);
}

public function test_connection_parameters_apply_dsn_part_overrides(): void
{
$this->bootKernel([
'config' => static function (TestKernel $kernel): void {
$kernel->addTestExtensionConfig('flow_postgresql', [
'connections' => [
'default' => [
'dsn' => 'postgresql://user:pass@localhost:5432/app',
'host' => 'db.internal',
'port' => 5544,
'user' => 'svc',
'password' => 'pw',
],
],
]);
},
]);

$params = $this->getContainer()->get('flow.postgresql.default.connection_parameters');
static::assertSame('db.internal', $params->host());
static::assertSame(5544, $params->port());
static::assertSame('svc', $params->user());
static::assertSame('pw', $params->password());
}

public function test_connection_parameters_apply_dbname_suffix(): void
{
$this->bootKernel([
'config' => static function (TestKernel $kernel): void {
$kernel->addTestExtensionConfig('flow_postgresql', [
'connections' => [
'default' => [
'dsn' => 'postgresql://postgres:postgres@localhost:5432/app',
'dbname_suffix' => '_test',
],
],
]);
},
]);

static::assertSame(
'app_test',
$this->getContainer()->get('flow.postgresql.default.connection_parameters')->database(),
);
}

public function test_connection_parameters_resolve_dbname_suffix_from_env(): void
{
$this->bootKernel([
'config' => static function (TestKernel $kernel): void {
$kernel->addTestContainerConfigurator(static function (ContainerBuilder $container): void {
$container->setParameter('env(FLOW_TEST_DB_SUFFIX)', '_test7');
});
$kernel->addTestExtensionConfig('flow_postgresql', [
'connections' => [
'default' => [
'dsn' => 'postgresql://postgres:postgres@localhost:5432/app',
'dbname_suffix' => '%env(FLOW_TEST_DB_SUFFIX)%',
],
],
]);
},
]);

static::assertSame(
'app_test7',
$this->getContainer()->get('flow.postgresql.default.connection_parameters')->database(),
);
}

public function test_connection_with_migrations_registers_migration_services(): void
{
$this->bootKernel([
Expand Down
Loading
Loading