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
14 changes: 12 additions & 2 deletions documentation/components/bridges/symfony-postgresql-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,9 @@ and failures.
```yaml
flow_postgresql:
profiler:
# enabled: null (default) auto-enables when WebProfilerBundle is registered; true forces it on
# (throws if WebProfilerBundle is absent); false forces it off entirely.
enabled: ~
include_parameters: true # show bound query parameters in the panel
migrations: true # show the Flow Migrations panel (requires migrations enabled)
```

Recording is dev-only and adds nothing in production: when the profiler is disabled — or
Expand All @@ -151,6 +150,17 @@ flow_postgresql:
profiler: false # do not record queries in selected connection (default: true)
```

When `migrations` are enabled, a separate **Flow Migrations** panel reports each connection's
executed, pending and unavailable migrations (with execution time) — like the Doctrine Migrations
bundle's panel. It queries the database on every profiled request; set `profiler.migrations: false`
to disable it while keeping the query panel:

```yaml
flow_postgresql:
profiler:
migrations: false # do not query/show migration status in the profiler (default: true)
```

### Migrations

Migrations are configured at the top level, not per connection. Use `--connection` to target a specific connection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,13 +475,17 @@ public function configure(DefinitionConfigurator $definition): void
->info('Show bound query parameters in the panel')
->defaultTrue()
->end()
->booleanNode('migrations')
->info('Show the Flow Migrations panel (executed/pending/unavailable status). Queries the database on every profiled request; set false to disable. Requires migrations to be enabled.')
->defaultTrue()
->end()
->end()
->end()
->end();
}

/**
* @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}, profiler?: bool}>, 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, drop_if_exists: bool, context?: array<string, mixed>, 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>}>, profiler?: array{enabled?: bool|null, include_parameters?: bool}} $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}, profiler?: bool}>, 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, drop_if_exists: bool, context?: array<string, mixed>, 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>}>, profiler?: array{enabled?: bool|null, include_parameters?: bool, migrations?: bool}} $config
*/
#[Override]
public function loadExtension(array $config, ContainerConfigurator $configurator, ContainerBuilder $container): void
Expand Down Expand Up @@ -520,6 +524,38 @@ public function loadExtension(array $config, ContainerConfigurator $configurator
$this->registerCache($config['cache'] ?? [], $connectionNames, $container);
$this->registerSession($config['session'] ?? [], $connectionNames, $container);
$this->registerProfiler($config, $configurator, $container, $connectionNames);
$this->registerMigrationsProfiler($config, $configurator, $container);
}

/**
* @param array<array-key, mixed> $config
*/
private function registerMigrationsProfiler(
array $config,
ContainerConfigurator $configurator,
ContainerBuilder $container,
): void {
$migrationsConfig = is_array($config['migrations'] ?? null) ? $config['migrations'] : [];

if (($migrationsConfig['enabled'] ?? false) !== true) {
return;
}

$profilerConfig = is_array($config['profiler'] ?? null) ? $config['profiler'] : [];
// @mago-expect analysis:mixed-assignment
$enabled = $profilerConfig['enabled'] ?? null;

if ($enabled === false || ($profilerConfig['migrations'] ?? true) === false) {
return;
}

$hasWebProfiler = $this->isWebProfilerBundleRegistered($container);

if ($enabled === null && !$hasWebProfiler) {
return;
}

$configurator->import(__DIR__ . '/Resources/config/profiler_migrations.php');
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

declare(strict_types=1);

namespace Flow\Bridge\Symfony\PostgreSqlBundle\Profiler;

use Flow\PostgreSql\Migrations\Configuration;
use Flow\PostgreSql\Migrations\MigrationState;
use Flow\PostgreSql\Migrations\Migrator;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Throwable;

use function array_map;
use function array_sum;
use function count;
use function Flow\Types\DSL\type_instance_of;

/**
* @phpstan-type MigrationRow array{version: string, name: string, state: string, executedAt: null|string, executionTimeMs: null|int}
* @phpstan-type ConfigurationData array{tableName: string, tableSchema: string, directory: string, namespace: string, allOrNothing: bool}
* @phpstan-type ConnectionData array{total: int, executed: int, pending: int, unavailable: int, migrations: list<MigrationRow>, configuration: null|ConfigurationData, error: null|string}
*/
final class FlowMigrationsDataCollector extends DataCollector
{
/**
* @param list<string> $connections
*/
public function __construct(
private readonly ContainerInterface $locator,
private readonly array $connections,
) {}

public function collect(Request $request, Response $response, ?Throwable $exception = null): void
{
if ($this->data !== []) {
return;
}

$connections = [];

foreach ($this->connections as $name) {
$connections[$name] = $this->collectConnection($name);
}

$this->data = ['connections' => $connections];
}

public function reset(): void
{
$this->data = [];
}

public function getName(): string
{
return 'flow_postgresql_migrations';
}

/**
* @return array<string, ConnectionData>
*/
public function getConnections(): array
{
// @mago-expect analysis:mixed-return-statement
return $this->data['connections'] ?? [];
}

public function getExecutedCount(): int
{
return $this->sum('executed');
}

public function getPendingCount(): int
{
return $this->sum('pending');
}

public function getUnavailableCount(): int
{
return $this->sum('unavailable');
}

public function getTotalCount(): int
{
return $this->sum('total');
}

/**
* @return ConnectionData
*/
private function collectConnection(string $name): array
{
try {
$configuration = $this->describeConfiguration($name);
$status = type_instance_of(Migrator::class)
->assert($this->locator->get("flow.postgresql.{$name}.migrations.migrator"))
->status();

$migrations = [];
$counts = [
MigrationState::EXECUTED->value => 0,
MigrationState::PENDING->value => 0,
MigrationState::UNAVAILABLE->value => 0,
];

foreach ($status as $migration) {
$counts[$migration->state->value]++;
$migrations[] = [
'version' => (string) $migration->version,
'name' => $migration->name,
'state' => $migration->state->value,
'executedAt' => $migration->executedAt?->format('Y-m-d H:i:s'),
'executionTimeMs' => $migration->executionTimeMs,
];
}

return [
'total' => count($status),
'executed' => $counts[MigrationState::EXECUTED->value],
'pending' => $counts[MigrationState::PENDING->value],
'unavailable' => $counts[MigrationState::UNAVAILABLE->value],
'migrations' => $migrations,
'configuration' => $configuration,
'error' => null,
];
} catch (Throwable $exception) {
return [
'total' => 0,
'executed' => 0,
'pending' => 0,
'unavailable' => 0,
'migrations' => [],
'configuration' => null,
'error' => $exception->getMessage(),
];
}
}

/**
* @return ConfigurationData
*/
private function describeConfiguration(string $name): array
{
$configuration = type_instance_of(Configuration::class)->assert($this->locator->get(
"flow.postgresql.{$name}.migrations.configuration",
));

return [
'tableName' => $configuration->tableName,
'tableSchema' => $configuration->tableSchema,
'directory' => $configuration->migrationsDirectory,
'namespace' => $configuration->migrationsNamespace,
'allOrNothing' => $configuration->allOrNothing,
];
}

/**
* @param 'executed'|'pending'|'total'|'unavailable' $key
*/
private function sum(string $key): int
{
return (int) array_sum(array_map(
static fn(array $connection): int => (int) ($connection[$key] ?? 0),
$this->getConnections(),
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

use Flow\Bridge\Symfony\PostgreSqlBundle\Profiler\FlowMigrationsDataCollector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;

return static function (ContainerConfigurator $container): void {
$services = $container->services();

$services
->set('flow.postgresql.profiler.migrations_collector', FlowMigrationsDataCollector::class)
->args([
service('flow.postgresql.command_locator'),
param('flow.postgresql.migrations.connections'),
])
->public()
->tag('data_collector', [
'id' => 'flow_postgresql_migrations',
'template' => '@FlowPostgreSql/Collector/migrations.html.twig',
'priority' => 239,
]);
};
Loading
Loading