From 6b14893cd7f1eeb86b0c8200c198552df2497a0d Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Wed, 29 Oct 2025 09:37:46 +0100 Subject: [PATCH 01/20] IBX-10764: Upsun env var loader --- .../IbexaCloudExtension.php | 11 + .../DependencyInjection/UpsunEnvVarLoader.php | 336 ++++++++++++++++++ .../TrustedHeaderClientIpEventSubscriber.php | 64 ++++ src/bundle/Resources/config/services.yaml | 1 + src/bundle/Resources/config/services/.gitkeep | 0 .../Resources/config/services/console.yaml | 6 + src/bundle/Resources/config/services/env.yaml | 6 + 7 files changed, 424 insertions(+) create mode 100644 src/bundle/DependencyInjection/UpsunEnvVarLoader.php create mode 100644 src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php delete mode 100644 src/bundle/Resources/config/services/.gitkeep create mode 100644 src/bundle/Resources/config/services/console.yaml create mode 100644 src/bundle/Resources/config/services/env.yaml diff --git a/src/bundle/DependencyInjection/IbexaCloudExtension.php b/src/bundle/DependencyInjection/IbexaCloudExtension.php index 08987c5..a822e52 100644 --- a/src/bundle/DependencyInjection/IbexaCloudExtension.php +++ b/src/bundle/DependencyInjection/IbexaCloudExtension.php @@ -38,6 +38,17 @@ public function prepend(ContainerBuilder $container): void { $this->prependDefaultConfiguration($container); $this->prependJMSTranslation($container); + + if (($_SERVER['HTTPCACHE_PURGE_TYPE'] ?? $_ENV['HTTPCACHE_PURGE_TYPE'] ?? null) === 'varnish') { + $container->setParameter('ibexa.http_cache.purge_type', 'varnish'); + } + + // Adapt config based on enabled PHP extensions + // Get imagine to use imagick if enabled, to avoid using php memory for image conversions + // Cannot be placed as env var due to how LiipImagineBundle processes its config + if (\extension_loaded('imagick')) { + $container->setParameter('liip_imagine_driver', 'imagick'); + } } private function prependDefaultConfiguration(ContainerBuilder $container): void diff --git a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php new file mode 100644 index 0000000..7a6aed7 --- /dev/null +++ b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php @@ -0,0 +1,336 @@ +decodePayload($relationshipsEncoded); + + $routes = $this->decodePayload($routesEncoded); + + if ($relationships === null || $routes === null) { + return []; + } + + return array_filter( + array_merge( + $this->buildDfsEnvVars($relationships), + $this->buildCacheEnvVars($relationships), + $this->buildSessionEnvVars($relationships), + $this->buildSearchEnvVars($relationships), + $this->buildVarnishEnvVars($routes), + ), + static fn (string|int|null $value): bool => $value !== null && $value !== '' + ); + } + + /** + * @param array>> $relationships + * + * @return array + */ + private function buildDfsEnvVars(array $relationships): array + { + $dfsPath = $_SERVER['PLATFORMSH_DFS_NFS_PATH'] ?? null; + if ($dfsPath === null) { + return []; + } + + $envVars = [ + $this->envKey('dfs_nfs_path') => $dfsPath, + $this->envKey('dfs_database_charset') => $_SERVER['DATABASE_CHARSET'] + ?? self::MYSQL_DEFAULT_DATABASE_CHARSET, + $this->envKey('dfs_database_collation') => $_SERVER['DATABASE_COLLATION'] + ?? self::DEFAULT_DATABASE_COLLATION, + ]; + + if (isset($relationships['dfs_database'])) { + foreach ($relationships['dfs_database'] as $endpoint) { + if (empty($endpoint['query']['is_master'])) { + continue; + } + + $pdoDriver = $this->normalizePdoDriver((string) ($endpoint['scheme'] ?? '')); + $envVars[$this->envKey('dfs_database_driver')] = $pdoDriver; + + // If driver is PGSQL, charset has to be set to utf8 + if ($pdoDriver === 'pdo_pgsql') { + $envVars[$this->envKey('dfs_database_charset')] = self::PGSQL_DEFAULT_DATABASE_CHARSET; + } + + $envVars[$this->envKey('dfs_database_url')] = sprintf( + '%s://%s:%s@%s:%d/%s', + $endpoint['scheme'], + $endpoint['username'], + $endpoint['password'], + $endpoint['host'], + $endpoint['port'], + ltrim((string) $endpoint['path'], '/') + ); + + break; + } + } else { + $driver = $this->guessRepositoryDriver(); + if ($driver !== null) { + $envVars[$this->envKey('dfs_database_driver')] = $driver; + } + } + + return $envVars; + } + + /** + * @param array>> $relationships + * + * @return array + */ + private function buildCacheEnvVars(array $relationships): array + { + if (isset($relationships['rediscache'])) { + foreach ($relationships['rediscache'] as $endpoint) { + if (($endpoint['scheme'] ?? null) !== 'redis') { + continue; + } + + return [ + $this->envKey('cache_pool') => 'cache.redis', + $this->envKey('cache_dsn') => sprintf( + '%s:%d?retry_interval=3', + $endpoint['host'], + $endpoint['port'], + ), + ]; + } + } + + if (isset($relationships['cache'])) { + foreach ($relationships['cache'] as $endpoint) { + if (($endpoint['scheme'] ?? null) !== 'memcached') { + continue; + } + + @trigger_error('Usage of Memcached is deprecated, redis is recommended', \E_USER_DEPRECATED); + + return [ + $this->envKey('cache_pool') => 'cache.memcached', + $this->envKey('cache_dsn') => sprintf('%s:%d', $endpoint['host'], $endpoint['port']), + ]; + } + } + + return []; + } + + /** + * @param array>> $relationships + * + * @return array + */ + private function buildSessionEnvVars(array $relationships): array + { + $endpoints = $relationships['redissession'] ?? $relationships['rediscache'] ?? null; + if ($endpoints === null) { + return []; + } + + foreach ($endpoints as $endpoint) { + if (($endpoint['scheme'] ?? null) !== 'redis') { + continue; + } + + return [ + $this->envKey('session_handler_id') => NativeSessionHandler::class, + $this->envKey('session_save_path') => sprintf( + '%s:%d', + $endpoint['host'], + $endpoint['port'], + ), + ]; + } + + return []; + } + + /** + * @param array>> $relationships + * + * @return array + */ + private function buildSearchEnvVars(array $relationships): array + { + $envVars = []; + + if (isset($relationships['solr'])) { + foreach ($relationships['solr'] as $endpoint) { + if (($endpoint['scheme'] ?? null) !== 'solr') { + continue; + } + + $envVars[$this->envKey('search_engine')] = 'solr'; + $envVars[$this->envKey('solr_dsn')] = sprintf( + 'http://%s:%d/%s', + $endpoint['host'], + $endpoint['port'], + 'solr' + ); + $envVars[$this->envKey('solr_core')] = substr((string) $endpoint['path'], 5); + } + } + + if (isset($relationships['elasticsearch'])) { + foreach ($relationships['elasticsearch'] as $endpoint) { + $dsn = sprintf('%s:%d', $endpoint['host'], $endpoint['port']); + + if (($endpoint['username'] ?? null) !== null && ($endpoint['password'] ?? null) !== null) { + $dsn = $endpoint['username'] . ':' . $endpoint['password'] . '@' . $dsn; + } + + if (($endpoint['path'] ?? null) !== null) { + $dsn .= '/' . ltrim((string) $endpoint['path'], '/'); + } + + $dsn = $endpoint['scheme'] . '://' . $dsn; + + $envVars[$this->envKey('search_engine')] = 'elasticsearch'; + $envVars[$this->envKey('elasticsearch_dsn')] = $dsn; + } + } + + return $envVars; + } + + /** + * @param array> $routes + * + * @return array + */ + private function buildVarnishEnvVars(array $routes): array + { + $envVars = []; + $varnishRoute = null; + + foreach ($routes as $host => $info) { + if ($varnishRoute === null && $this->isVarnishRoute($info)) { + $varnishRoute = $host; + } + + if ($this->isVarnishRoute($info) && ($info['primary'] ?? false) === true) { + $varnishRoute = $host; + break; + } + } + + if ($varnishRoute !== null && !($_SERVER['SKIP_HTTPCACHE_PURGE'] ?? false)) { + $purgeServer = rtrim($varnishRoute, '/'); + $username = $_SERVER['HTTPCACHE_USERNAME'] ?? null; + $password = $_SERVER['HTTPCACHE_PASSWORD'] ?? null; + + if ($username !== null && $password !== null) { + $domain = parse_url($purgeServer, PHP_URL_HOST); + if (\is_string($domain) && $domain !== '') { + $credentials = rawurlencode($username) . ':' . rawurlencode($password); + $purgeServer = str_replace($domain, $credentials . '@' . $domain, $purgeServer); + } + } + + $envVars[$this->envKey('httpcache_purge_type')] = 'varnish'; + $envVars[$this->envKey('httpcache_purge_server')] = $purgeServer; + } + + $envVars[$this->envKey('httpcache_varnish_invalidate_token')] = $_SERVER['HTTPCACHE_VARNISH_INVALIDATE_TOKEN'] + ?? $_SERVER['PLATFORM_PROJECT_ENTROPY'] + ?? ''; + + return $envVars; + } + + /** + * @return array|null + */ + private function decodePayload(string $payload): ?array + { + $decoded = base64_decode($payload, true); + if ($decoded === false) { + return null; + } + + try { + /** @var array $data */ + return json_decode($decoded, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException) { + return null; + } + } + + private function normalizePdoDriver(string $scheme): string + { + if ($scheme === '') { + return ''; + } + + return str_starts_with($scheme, 'pdo_') ? $scheme : 'pdo_' . $scheme; + } + + private function guessRepositoryDriver(): ?string + { + $explicit = $this->getFirstNonEmptyEnv('DATABASE_DRIVER'); + if ($explicit !== null) { + return $explicit; + } + + $databaseUrl = $this->getFirstNonEmptyEnv('DATABASE_URL'); + if ($databaseUrl === null) { + return null; + } + + $scheme = parse_url($databaseUrl, PHP_URL_SCHEME); + if (!\is_string($scheme) || $scheme === '') { + return null; + } + + return $this->normalizePdoDriver($scheme); + } + + private function envKey(string $parameterName): string + { + return strtoupper(str_replace(['.', '-'], '_', $parameterName)); + } + + /** + * @param array $route + */ + private function isVarnishRoute(array $route): bool + { + return ($route['type'] ?? null) === 'upstream' && ($route['upstream'] ?? null) === 'varnish'; + } + + private function getFirstNonEmptyEnv(string $name): ?string + { + $value = $_SERVER[$name] ?? $_ENV[$name] ?? null; + $value = $value === '' ? null : $value; + + return \is_string($value) ? $value : null; + } +} diff --git a/src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php b/src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php new file mode 100644 index 0000000..f592865 --- /dev/null +++ b/src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php @@ -0,0 +1,64 @@ + ['onKernelRequest', PHP_INT_MAX], + ]; + } + + public function onKernelRequest(RequestEvent $event): void + { + $request = $event->getRequest(); + + $trustedProxies = Request::getTrustedProxies(); + $trustedHeaderSet = Request::getTrustedHeaderSet(); + + $trustedHeaderName = $this->trustedHeaderName; + if (null === $trustedHeaderName && $this->isUpsunProxy($request)) { + $trustedHeaderName = self::PLATFORM_SH_TRUSTED_HEADER_CLIENT_IP; + } + + if (null === $trustedHeaderName) { + return; + } + + $trustedClientIp = $request->headers->get($trustedHeaderName); + + if (null !== $trustedClientIp) { + if ($trustedHeaderSet !== -1) { + $trustedHeaderSet |= Request::HEADER_X_FORWARDED_FOR; + } + $request->headers->set('X_FORWARDED_FOR', $trustedClientIp); + } + + Request::setTrustedProxies($trustedProxies, $trustedHeaderSet); + } + + private function isUpsunProxy(Request $request): bool + { + return null !== $request->server->get('PLATFORM_RELATIONSHIPS'); + } +} diff --git a/src/bundle/Resources/config/services.yaml b/src/bundle/Resources/config/services.yaml index a6bdf1d..fb1ddef 100644 --- a/src/bundle/Resources/config/services.yaml +++ b/src/bundle/Resources/config/services.yaml @@ -1,2 +1,3 @@ imports: - { resource: services/**/*.yaml } + diff --git a/src/bundle/Resources/config/services/.gitkeep b/src/bundle/Resources/config/services/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/bundle/Resources/config/services/console.yaml b/src/bundle/Resources/config/services/console.yaml new file mode 100644 index 0000000..0c20987 --- /dev/null +++ b/src/bundle/Resources/config/services/console.yaml @@ -0,0 +1,6 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + Ibexa\Cloud\Command\IbexaSetupCommand: ~ \ No newline at end of file diff --git a/src/bundle/Resources/config/services/env.yaml b/src/bundle/Resources/config/services/env.yaml new file mode 100644 index 0000000..6ae0b7f --- /dev/null +++ b/src/bundle/Resources/config/services/env.yaml @@ -0,0 +1,6 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + Ibexa\Bundle\Cloud\DependencyInjection\UpsunEnvVarLoader: ~ From 1dcb8a67cd5492946ec42764f34f6b245521519c Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Thu, 30 Oct 2025 12:51:13 +0100 Subject: [PATCH 02/20] IBX-10764: Add unit tests --- .../DependencyInjection/UpsunEnvVarLoader.php | 14 +- .../UpsunEnvVarLoaderTest.php | 363 ++++++++++++++++++ 2 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php diff --git a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php index 7a6aed7..e36f82c 100644 --- a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php +++ b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php @@ -1,5 +1,9 @@ decodePayload($relationshipsEncoded); - $routes = $this->decodePayload($routesEncoded); if ($relationships === null || $routes === null) { @@ -67,7 +70,7 @@ private function buildDfsEnvVars(array $relationships): array if (isset($relationships['dfs_database'])) { foreach ($relationships['dfs_database'] as $endpoint) { - if (empty($endpoint['query']['is_master'])) { + if (!isset($endpoint['query']['is_master'])) { continue; } @@ -131,7 +134,7 @@ private function buildCacheEnvVars(array $relationships): array continue; } - @trigger_error('Usage of Memcached is deprecated, redis is recommended', \E_USER_DEPRECATED); + @trigger_error('Usage of Memcached is deprecated, redis is recommended', E_USER_DEPRECATED); return [ $this->envKey('cache_pool') => 'cache.memcached', @@ -242,7 +245,9 @@ private function buildVarnishEnvVars(array $routes): array } } - if ($varnishRoute !== null && !($_SERVER['SKIP_HTTPCACHE_PURGE'] ?? false)) { + $skipHttpCachePurge = (bool) ($_SERVER['SKIP_HTTPCACHE_PURGE'] ?? false); + + if ($varnishRoute !== null && $skipHttpCachePurge === false) { $purgeServer = rtrim($varnishRoute, '/'); $username = $_SERVER['HTTPCACHE_USERNAME'] ?? null; $password = $_SERVER['HTTPCACHE_PASSWORD'] ?? null; @@ -277,7 +282,6 @@ private function decodePayload(string $payload): ?array } try { - /** @var array $data */ return json_decode($decoded, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException) { return null; diff --git a/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php b/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php new file mode 100644 index 0000000..64cadd0 --- /dev/null +++ b/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php @@ -0,0 +1,363 @@ + */ + private array $originalServer; + + protected function setUp(): void + { + parent::setUp(); + + $this->originalServer = $_SERVER; + } + + protected function tearDown(): void + { + $_SERVER = $this->originalServer; + + parent::tearDown(); + } + + /** + * @param array>> $relationships + * @param array> $routes + * @param array $expectedEnv + * @param array $serverValues + * + * @dataProvider providerForTestLoadEnvVars + */ + public function testLoadEnvVars( + array $relationships, + array $routes, + array $expectedEnv, + array $serverValues + ): void { + $_SERVER = $this->originalServer; + + foreach ($serverValues as $key => $value) { + $_SERVER[$key] = $value; + } + + $_SERVER['PLATFORM_RELATIONSHIPS'] = base64_encode(json_encode($relationships, JSON_THROW_ON_ERROR)); + $_SERVER['PLATFORM_ROUTES'] = base64_encode(json_encode($routes, JSON_THROW_ON_ERROR)); + + $loader = new UpsunEnvVarLoader(); + $result = $loader->loadEnvVars(); + + self::assertSame($expectedEnv, $result); + } + + /** + * @return iterable< + * string, + * array{ + * array>>, + * array>, + * array, + * array + * } + * > + */ + public function providerForTestLoadEnvVars(): iterable + { + $relationships = [ + 'database' => [ + $this->createDatabase(), + ], + 'rediscache' => [ + $this->createRedisCache(), + ], + ]; + $routes = $this->createRoutes(); + + $expected = [ + 'CACHE_POOL' => 'cache.redis', + 'CACHE_DSN' => 'rediscache.internal:6379?retry_interval=3', + 'SESSION_HANDLER_ID' => NativeSessionHandler::class, + 'SESSION_SAVE_PATH' => 'rediscache.internal:6379', + 'SEARCH_ENGINE' => 'elasticsearch', + 'ELASTICSEARCH_DSN' => 'http://elasticsearch.internal:9200', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ]; + + $serverValues = [ + 'PLATFORM_PROJECT_ENTROPY' => 'project_entropy', + ]; + + yield 'redis cache with session fallback and elasticsearch' => [ + $relationships + ['elasticsearch' => [ + $this->createElasticSearch(), + ]], + $routes, + $expected, + $serverValues, + ]; + + $expected = [ + 'CACHE_POOL' => 'cache.redis', + 'CACHE_DSN' => 'rediscache.internal:6379?retry_interval=3', + 'SESSION_HANDLER_ID' => NativeSessionHandler::class, + 'SESSION_SAVE_PATH' => 'rediscache.internal:6379', + 'SEARCH_ENGINE' => 'solr', + 'SOLR_DSN' => 'http://solr.internal:8080/solr', + 'SOLR_CORE' => 'collection1', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ]; + + yield 'redis cache with session fallback and solr' => [ + $relationships + ['solr' => [ + $this->createSolr(), + ]], + $routes, + $expected, + $serverValues, + ]; + } + + /** + * @return array< + * string, + * array{ + * id: null, + * original_url: string, + * primary: bool, + * production_url: string, + * type: string, + * to?: string, + * attributes?: array, + * upstream?: string + * } + * > + */ + private function createRoutes(): array + { + return [ + 'http://app.example.com/' => [ + 'id' => null, + 'original_url' => 'http://some_app.example.com', + 'primary' => false, + 'production_url' => 'http://some_app.example.com/', + 'to' => 'https://app.example.com/', + 'type' => 'redirect', + ], + 'http://www.app.example.com/' => [ + 'id' => null, + 'original_url' => 'http://www.{default}/', + 'primary' => false, + 'production_url' => 'http://www.some_app.example.com/', + 'to' => 'https://www.app.example.com/', + 'type' => 'redirect', + ], + 'https://app.example.com/' => [ + 'attributes' => [], + 'id' => null, + 'original_url' => 'https://some_app.example.com', + 'primary' => true, + 'production_url' => 'https://some_app.example.com/', + 'type' => 'upstream', + 'upstream' => 'app', + ], + 'https://www.app.example.com/' => [ + 'attributes' => [], + 'id' => null, + 'original_url' => 'https://www.{default}/', + 'primary' => false, + 'production_url' => 'https://www.some_app.example.com/', + 'to' => 'https://app.example.com/', + 'type' => 'redirect', + ], + ]; + } + + /** + * @return array{ + * host: string, + * hostname: string, + * cluster: string, + * service: string, + * rel: string, + * scheme: string, + * username: string, + * password: string, + * port: int, + * epoch: int, + * path: string, + * query: array, + * fragment: null, + * public: bool, + * host_mapped: bool, + * type: string, + * instance_ips: array, + * ip: string + * } + */ + private function createDatabase(): array + { + return [ + 'host' => 'database.internal', + 'hostname' => 'mysql_db_random._.eu-4.platformsh.site', + 'cluster' => 'some_cluster', + 'service' => 'mysqldb', + 'rel' => 'user', + 'scheme' => 'mysql', + 'username' => 'user', + 'password' => 'some_password', + 'port' => 3306, + 'epoch' => 0, + 'path' => 'main', + 'query' => ['is_master' => true], + 'fragment' => null, + 'public' => false, + 'host_mapped' => false, + 'type' => 'mariadb:10.4', + 'instance_ips' => ['127.0.0.1'], + 'ip' => '127.0.0.1', + ]; + } + + /** + * @return array{ + * host: string, + * hostname: string, + * cluster: string, + * service: string, + * rel: string, + * scheme: string, + * username: null, + * password: null, + * port: int, + * epoch: int, + * path: null, + * query: array, + * fragment: null, + * public: bool, + * host_mapped: bool, + * type: string, + * instance_ips: array, + * ip: string + * } + */ + private function createRedisCache(): array + { + return [ + 'host' => 'rediscache.internal', + 'hostname' => 'redis.service._.eu-4.platformsh.site', + 'cluster' => 'some_cluster', + 'service' => 'rediscache', + 'rel' => 'redis', + 'scheme' => 'redis', + 'username' => null, + 'password' => null, + 'port' => 6379, + 'epoch' => 0, + 'path' => null, + 'query' => [], + 'fragment' => null, + 'public' => false, + 'host_mapped' => false, + 'type' => 'redis:5.0', + 'instance_ips' => ['127.0.0.1'], + 'ip' => '127.0.0.1', + ]; + } + + /** + * @return array{ + * username: null, + * scheme: string, + * service: string, + * fragment: null, + * ip: string, + * hostname: string, + * port: int, + * cluster: string, + * host: string, + * rel: string, + * path: null, + * query: array, + * password: string, + * type: string, + * public: bool, + * host_mapped: bool + * } + */ + private function createElasticSearch(): array + { + return [ + 'username' => null, + 'scheme' => 'http', + 'service' => 'elasticsearch', + 'fragment' => null, + 'ip' => '123.456.78.90', + 'hostname' => 'azertyuiopqsdfghjklm.elasticsearch.service._.eu-1.platformsh.site', + 'port' => 9200, + 'cluster' => 'azertyuiopqsdf-main-7rqtwti', + 'host' => 'elasticsearch.internal', + 'rel' => 'elasticsearch', + 'path' => null, + 'query' => [], + 'password' => 'ChangeMe', + 'type' => 'elasticsearch:8.5', + 'public' => false, + 'host_mapped' => false, + ]; + } + + /** + * @return array{ + * username: null, + * scheme: string, + * service: string, + * fragment: null, + * ip: string, + * hostname: string, + * port: int, + * cluster: string, + * host: string, + * rel: string, + * path: string, + * query: array, + * password: null, + * type: string, + * public: bool, + * host_mapped: bool + * } + */ + private function createSolr(): array + { + return [ + 'username' => null, + 'scheme' => 'solr', + 'service' => 'solr', + 'fragment' => null, + 'ip' => '123.456.78.90', + 'hostname' => 'host.solr.service._.eu-1.platformsh.site', + 'port' => 8080, + 'cluster' => 'some-cluster', + 'host' => 'solr.internal', + 'rel' => 'solr', + 'path' => 'solr/collection1', + 'query' => [], + 'password' => null, + 'type' => 'solr:9.9', + 'public' => false, + 'host_mapped' => false, + ]; + } +} From 50e1948cf4317df084b08f6a9ec8075c1d0a6a68 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Fri, 31 Oct 2025 10:12:07 +0100 Subject: [PATCH 03/20] IBX-10764: Add unit tests for dfs --- .../TrustedHeaderClientIpEventSubscriber.php | 2 +- .../UpsunEnvVarLoaderTest.php | 50 +++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php b/src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php index f592865..4f4dcb8 100644 --- a/src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php +++ b/src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php @@ -25,7 +25,7 @@ public function __construct( public static function getSubscribedEvents(): array { return [ - KernelEvents::REQUEST => ['onKernelRequest', PHP_INT_MAX], + KernelEvents::REQUEST => ['onKernelRequest', PHP_INT_MAX - 1], ]; } diff --git a/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php b/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php index 64cadd0..9ff59dc 100644 --- a/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php +++ b/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php @@ -36,9 +36,9 @@ protected function tearDown(): void /** * @param array>> $relationships - * @param array> $routes - * @param array $expectedEnv - * @param array $serverValues + * @param array> $routes + * @param array $expectedEnv + * @param array $serverValues * * @dataProvider providerForTestLoadEnvVars */ @@ -128,6 +128,22 @@ public function providerForTestLoadEnvVars(): iterable $expected, $serverValues, ]; + + $expected = [ + 'DFS_NFS_PATH' => '/mnt/dfs/nfs', + 'DFS_DATABASE_CHARSET' => 'utf8mb4', + 'DFS_DATABASE_COLLATION' => 'utf8mb4_unicode_520_ci', + 'DFS_DATABASE_DRIVER' => 'pdo_mysql', + 'DFS_DATABASE_URL' => 'mysql://dfs:dfs@localhost:3306/dfs', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ]; + + yield 'dfs' => [ + ['dfs_database' => [$this->createDfs()]], + $routes, + $expected, + $serverValues + ['PLATFORMSH_DFS_NFS_PATH' => '/mnt/dfs/nfs'], + ]; } /** @@ -360,4 +376,32 @@ private function createSolr(): array 'host_mapped' => false, ]; } + + /** + * @return array{ + * host: string, + * scheme: string, + * username: string, + * password: string, + * port: int, + * path: string, + * query: array{is_master: bool} + * } + */ + private function createDfs(): array + { + $parts = parse_url('mysql://dfs:dfs@localhost:3306/dfs'); + + return [ + 'host' => $parts['host'], + 'scheme' => $parts['scheme'], + 'username' => $parts['user'], + 'password' => $parts['pass'], + 'port' => $parts['port'], + 'path' => ltrim($parts['path'], '/'), + 'query' => [ + 'is_master' => true, + ], + ]; + } } From 33c2aea3694e94e653dda8139f52eae8a8a8a83c Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Fri, 31 Oct 2025 10:14:39 +0100 Subject: [PATCH 04/20] IBX-10764: Baseline --- phpstan-baseline.neon | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 phpstan-baseline.neon diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..cf8431e --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,7 @@ +parameters: + ignoreErrors: + - + message: '#^Parameter \#2 \$trustedHeaderSet of static method Symfony\\Component\\HttpFoundation\\Request\:\:setTrustedProxies\(\) expects int\<0, 63\>, int given\.$#' + identifier: argument.type + count: 1 + path: src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php From c58d245bcea691d59d788c7e0f46fdc78aba5a29 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 3 Nov 2025 22:23:03 +0100 Subject: [PATCH 05/20] IBX-10764: Bundle boot --- src/bundle/IbexaCloudBundle.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/bundle/IbexaCloudBundle.php b/src/bundle/IbexaCloudBundle.php index 70c3ce4..84fb57c 100644 --- a/src/bundle/IbexaCloudBundle.php +++ b/src/bundle/IbexaCloudBundle.php @@ -8,8 +8,21 @@ namespace Ibexa\Bundle\Cloud; +use Ibexa\Bundle\Cloud\DependencyInjection\UpsunEnvVarLoader; use Symfony\Component\HttpKernel\Bundle\Bundle; final class IbexaCloudBundle extends Bundle { + public function boot(): void + { + $envVars = (new UpsunEnvVarLoader())->loadEnvVars(); + + foreach ($envVars as $name => $value) { + $value = (string) $value; + + putenv($name . '=' . $value); + $_ENV[$name] = $value; + $_SERVER[$name] = $value; + } + } } From 7e6946e0635835a2cdfa4a45b72b1991ff3f1012 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Wed, 5 Nov 2025 01:07:15 +0100 Subject: [PATCH 06/20] IBX-10764: Add test env --- src/bundle/IbexaCloudBundle.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bundle/IbexaCloudBundle.php b/src/bundle/IbexaCloudBundle.php index 84fb57c..163a2a6 100644 --- a/src/bundle/IbexaCloudBundle.php +++ b/src/bundle/IbexaCloudBundle.php @@ -16,6 +16,7 @@ final class IbexaCloudBundle extends Bundle public function boot(): void { $envVars = (new UpsunEnvVarLoader())->loadEnvVars(); + $envVars = ['TEST_ENV_VAR' => 'test']; foreach ($envVars as $name => $value) { $value = (string) $value; From 72abd61294a300d46f1f1c43d2c370398407ea9c Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Fri, 28 Nov 2025 14:13:04 +0100 Subject: [PATCH 07/20] IBX-10764: Fixup --- src/bundle/IbexaCloudBundle.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/bundle/IbexaCloudBundle.php b/src/bundle/IbexaCloudBundle.php index 163a2a6..5ec6107 100644 --- a/src/bundle/IbexaCloudBundle.php +++ b/src/bundle/IbexaCloudBundle.php @@ -16,8 +16,6 @@ final class IbexaCloudBundle extends Bundle public function boot(): void { $envVars = (new UpsunEnvVarLoader())->loadEnvVars(); - $envVars = ['TEST_ENV_VAR' => 'test']; - foreach ($envVars as $name => $value) { $value = (string) $value; From 572e4a501ffb6bd8788c8f61df70e4a0bb071610 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Fri, 28 Nov 2025 15:02:59 +0100 Subject: [PATCH 08/20] IBX-10764: Added container configuration --- .../IbexaCloudExtension.php | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/bundle/DependencyInjection/IbexaCloudExtension.php b/src/bundle/DependencyInjection/IbexaCloudExtension.php index a822e52..f86ed57 100644 --- a/src/bundle/DependencyInjection/IbexaCloudExtension.php +++ b/src/bundle/DependencyInjection/IbexaCloudExtension.php @@ -36,6 +36,7 @@ public function load(array $configs, ContainerBuilder $container): void public function prepend(ContainerBuilder $container): void { + $this->configureUpsunSetup($container); $this->prependDefaultConfiguration($container); $this->prependJMSTranslation($container); @@ -84,4 +85,101 @@ private function shouldLoadTestServices(ContainerBuilder $container): bool return $container->hasParameter('ibexa.behat.browser.enabled') && true === $container->getParameter('ibexa.behat.browser.enabled'); } + + private function configureUpsunSetup(ContainerBuilder $container): void + { + $envVars = (new UpsunEnvVarLoader())->loadEnvVars(); + + if ($envVars === []) { + return; + } + + $projectDir = $container->getParameter('kernel.project_dir'); + + // Map environment variables to container parameters + $this->setParametersFromEnvVars($container, $envVars, $projectDir); + + // Load additional YAML configuration files based on enabled services + $this->loadServiceConfigurations($container, $envVars, $projectDir); + } + + /** + * @param array $envVars + */ + private function setParametersFromEnvVars( + ContainerBuilder $container, + array $envVars, + string $projectDir + ): void { + $parameterMapping = [ + 'DFS_DATABASE_DRIVER' => 'dfs_database_driver', + 'DFS_DATABASE_URL' => 'dfs_database_url', + 'DFS_DATABASE_CHARSET' => 'dfs_database_charset', + 'DFS_DATABASE_COLLATION' => 'dfs_database_collation', + 'CACHE_POOL' => 'cache_pool', + 'CACHE_DSN' => 'cache_dsn', + 'SESSION_HANDLER_ID' => 'ibexa.session.handler_id', + 'SESSION_SAVE_PATH' => 'ibexa.session.save_path', + 'SEARCH_ENGINE' => 'search_engine', + 'SOLR_DSN' => 'solr_dsn', + 'SOLR_CORE' => 'solr_core', + 'ELASTICSEARCH_DSN' => 'elasticsearch_dsn', + 'HTTPCACHE_PURGE_TYPE' => ['purge_type', 'ibexa.http_cache.purge_type'], + 'HTTPCACHE_PURGE_SERVER' => 'purge_server', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'varnish_invalidate_token', + ]; + + foreach ($envVars as $envKey => $value) { + if (!isset($parameterMapping[$envKey])) { + continue; + } + + $parameters = (array) $parameterMapping[$envKey]; + foreach ($parameters as $parameterName) { + $container->setParameter($parameterName, $value); + } + } + + // Handle DFS_NFS_PATH specially - convert from relative to absolute path + if (isset($envVars['DFS_NFS_PATH'])) { + $absolutePath = sprintf('%s/%s', $projectDir, $envVars['DFS_NFS_PATH']); + $container->setParameter('dfs_nfs_path', $absolutePath); + } + } + + /** + * @param array $envVars + */ + private function loadServiceConfigurations( + ContainerBuilder $container, + array $envVars, + string $projectDir + ): void { + // Load DFS configuration if DFS is enabled + if (isset($envVars['DFS_NFS_PATH'])) { + $loader = new YamlFileLoader( + $container, + new FileLocator($projectDir . '/config/packages/dfs') + ); + $loader->load('dfs.yaml'); + } + + // Load cache configuration based on cache type + if (isset($envVars['CACHE_POOL'])) { + $cacheType = $envVars['CACHE_POOL']; + $configFile = match ($cacheType) { + 'cache.redis' => 'cache.redis.yaml', + 'cache.memcached' => 'cache.memcached.yaml', + default => null, + }; + + if ($configFile !== null) { + $loader = new YamlFileLoader( + $container, + new FileLocator($projectDir . '/config/packages/cache_pool') + ); + $loader->load($configFile); + } + } + } } From dc65c7e45dcdbed01e69b545107acfb45783c4fc Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Fri, 28 Nov 2025 16:24:27 +0100 Subject: [PATCH 09/20] IBX-10764: Refactor service files loading --- .../IbexaCloudExtension.php | 93 ++++--------------- 1 file changed, 20 insertions(+), 73 deletions(-) diff --git a/src/bundle/DependencyInjection/IbexaCloudExtension.php b/src/bundle/DependencyInjection/IbexaCloudExtension.php index f86ed57..7a51caf 100644 --- a/src/bundle/DependencyInjection/IbexaCloudExtension.php +++ b/src/bundle/DependencyInjection/IbexaCloudExtension.php @@ -90,61 +90,7 @@ private function configureUpsunSetup(ContainerBuilder $container): void { $envVars = (new UpsunEnvVarLoader())->loadEnvVars(); - if ($envVars === []) { - return; - } - - $projectDir = $container->getParameter('kernel.project_dir'); - - // Map environment variables to container parameters - $this->setParametersFromEnvVars($container, $envVars, $projectDir); - - // Load additional YAML configuration files based on enabled services - $this->loadServiceConfigurations($container, $envVars, $projectDir); - } - - /** - * @param array $envVars - */ - private function setParametersFromEnvVars( - ContainerBuilder $container, - array $envVars, - string $projectDir - ): void { - $parameterMapping = [ - 'DFS_DATABASE_DRIVER' => 'dfs_database_driver', - 'DFS_DATABASE_URL' => 'dfs_database_url', - 'DFS_DATABASE_CHARSET' => 'dfs_database_charset', - 'DFS_DATABASE_COLLATION' => 'dfs_database_collation', - 'CACHE_POOL' => 'cache_pool', - 'CACHE_DSN' => 'cache_dsn', - 'SESSION_HANDLER_ID' => 'ibexa.session.handler_id', - 'SESSION_SAVE_PATH' => 'ibexa.session.save_path', - 'SEARCH_ENGINE' => 'search_engine', - 'SOLR_DSN' => 'solr_dsn', - 'SOLR_CORE' => 'solr_core', - 'ELASTICSEARCH_DSN' => 'elasticsearch_dsn', - 'HTTPCACHE_PURGE_TYPE' => ['purge_type', 'ibexa.http_cache.purge_type'], - 'HTTPCACHE_PURGE_SERVER' => 'purge_server', - 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'varnish_invalidate_token', - ]; - - foreach ($envVars as $envKey => $value) { - if (!isset($parameterMapping[$envKey])) { - continue; - } - - $parameters = (array) $parameterMapping[$envKey]; - foreach ($parameters as $parameterName) { - $container->setParameter($parameterName, $value); - } - } - - // Handle DFS_NFS_PATH specially - convert from relative to absolute path - if (isset($envVars['DFS_NFS_PATH'])) { - $absolutePath = sprintf('%s/%s', $projectDir, $envVars['DFS_NFS_PATH']); - $container->setParameter('dfs_nfs_path', $absolutePath); - } + $this->loadServiceConfigurations($container, $envVars); } /** @@ -153,9 +99,9 @@ private function setParametersFromEnvVars( private function loadServiceConfigurations( ContainerBuilder $container, array $envVars, - string $projectDir ): void { - // Load DFS configuration if DFS is enabled + $projectDir = $container->getParameter('kernel.project_dir'); + if (isset($envVars['DFS_NFS_PATH'])) { $loader = new YamlFileLoader( $container, @@ -164,22 +110,23 @@ private function loadServiceConfigurations( $loader->load('dfs.yaml'); } - // Load cache configuration based on cache type - if (isset($envVars['CACHE_POOL'])) { - $cacheType = $envVars['CACHE_POOL']; - $configFile = match ($cacheType) { - 'cache.redis' => 'cache.redis.yaml', - 'cache.memcached' => 'cache.memcached.yaml', - default => null, - }; - - if ($configFile !== null) { - $loader = new YamlFileLoader( - $container, - new FileLocator($projectDir . '/config/packages/cache_pool') - ); - $loader->load($configFile); - } + if (!isset($envVars['CACHE_POOL'])) { + return; + } + + $cacheType = $envVars['CACHE_POOL']; + $configFile = match ($cacheType) { + 'cache.redis' => 'cache.redis.yaml', + 'cache.memcached' => 'cache.memcached.yaml', + default => null, + }; + + if ($configFile !== null) { + $loader = new YamlFileLoader( + $container, + new FileLocator($projectDir . '/config/packages/cache_pool') + ); + $loader->load($configFile); } } } From 566c78bc81e09bb1805783a52acf024db7a472f5 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 1 Dec 2025 21:58:41 +0100 Subject: [PATCH 10/20] IBX-10764: Added database URLs handling --- .../IbexaCloudExtension.php | 43 ++-- .../DependencyInjection/UpsunEnvVarLoader.php | 74 +++++++ .../UpsunEnvVarLoaderTest.php | 183 +++++++++++++++++- 3 files changed, 273 insertions(+), 27 deletions(-) diff --git a/src/bundle/DependencyInjection/IbexaCloudExtension.php b/src/bundle/DependencyInjection/IbexaCloudExtension.php index 7a51caf..9b90afb 100644 --- a/src/bundle/DependencyInjection/IbexaCloudExtension.php +++ b/src/bundle/DependencyInjection/IbexaCloudExtension.php @@ -39,17 +39,6 @@ public function prepend(ContainerBuilder $container): void $this->configureUpsunSetup($container); $this->prependDefaultConfiguration($container); $this->prependJMSTranslation($container); - - if (($_SERVER['HTTPCACHE_PURGE_TYPE'] ?? $_ENV['HTTPCACHE_PURGE_TYPE'] ?? null) === 'varnish') { - $container->setParameter('ibexa.http_cache.purge_type', 'varnish'); - } - - // Adapt config based on enabled PHP extensions - // Get imagine to use imagick if enabled, to avoid using php memory for image conversions - // Cannot be placed as env var due to how LiipImagineBundle processes its config - if (\extension_loaded('imagick')) { - $container->setParameter('liip_imagine_driver', 'imagick'); - } } private function prependDefaultConfiguration(ContainerBuilder $container): void @@ -90,17 +79,17 @@ private function configureUpsunSetup(ContainerBuilder $container): void { $envVars = (new UpsunEnvVarLoader())->loadEnvVars(); - $this->loadServiceConfigurations($container, $envVars); - } + if (($_SERVER['HTTPCACHE_PURGE_TYPE'] ?? $_ENV['HTTPCACHE_PURGE_TYPE'] ?? null) === 'varnish') { + $container->setParameter('ibexa.http_cache.purge_type', 'varnish'); + } + + // Cannot be placed as env var due to how LiipImagineBundle processes its config + if (\extension_loaded('imagick')) { + $container->setParameter('liip_imagine_driver', 'imagick'); + } - /** - * @param array $envVars - */ - private function loadServiceConfigurations( - ContainerBuilder $container, - array $envVars, - ): void { $projectDir = $container->getParameter('kernel.project_dir'); + assert(is_string($projectDir)); if (isset($envVars['DFS_NFS_PATH'])) { $loader = new YamlFileLoader( @@ -121,12 +110,14 @@ private function loadServiceConfigurations( default => null, }; - if ($configFile !== null) { - $loader = new YamlFileLoader( - $container, - new FileLocator($projectDir . '/config/packages/cache_pool') - ); - $loader->load($configFile); + if ($configFile === null) { + return; } + + $loader = new YamlFileLoader( + $container, + new FileLocator($projectDir . '/config/packages/cache_pool') + ); + $loader->load($configFile); } } diff --git a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php index e36f82c..9bffd3a 100644 --- a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php +++ b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php @@ -38,6 +38,7 @@ public function loadEnvVars(): array return array_filter( array_merge( + $this->buildDatabaseEnvVars($relationships), $this->buildDfsEnvVars($relationships), $this->buildCacheEnvVars($relationships), $this->buildSessionEnvVars($relationships), @@ -48,6 +49,79 @@ public function loadEnvVars(): array ); } + /** + * @param array>> $relationships + * + * @return array + */ + private function buildDatabaseEnvVars(array $relationships): array + { + $envVars = []; + + foreach ($relationships as $key => $allValues) { + // Uppercase the key: "database" -> "DATABASE", "database_site" -> "DATABASE_SITE" + $key = strtoupper($key); + + foreach ($allValues as $i => $endpoint) { + $scheme = $endpoint['scheme'] ?? ''; + $isPGSQL = str_starts_with($scheme, 'pgsql'); + $isMySQL = str_starts_with($scheme, 'mysql'); + + if (!$isPGSQL && !$isMySQL) { + continue; + } + + // Build prefix: first endpoint gets "DATABASE_", second gets "DATABASE_1_", etc. + $prefix = $i === 0 ? "{$key}_" : "{$key}_{$i}_"; + // Replace hyphens with underscores + $prefix = str_replace('-', '_', $prefix); + + // Normalize scheme for PostgreSQL + if ($isPGSQL) { + $scheme = 'postgres'; + } + + $username = $endpoint['username'] ?? ''; + $password = $endpoint['password'] ?? ''; + $host = $endpoint['host'] ?? ''; + $port = $endpoint['port'] ?? 0; + $path = $endpoint['path'] ?? 'main'; + + // Build URL + $url = sprintf('%s://', $scheme); + if ($username !== '') { + $url .= $username; + if ($password !== '') { + $url .= ':' . $password; + } + $url .= '@'; + } + $url .= sprintf('%s:%s/%s?sslmode=disable', $host, $port, $path); + + // Add charset + $charset = $isMySQL ? self::MYSQL_DEFAULT_DATABASE_CHARSET : self::PGSQL_DEFAULT_DATABASE_CHARSET; + $url .= '&charset=' . $charset; + + // Add serverVersion for PHP/Doctrine + $type = $endpoint['type'] ?? null; + if ($type !== null && str_contains((string) $type, ':')) { + [, $version] = explode(':', (string) $type, 2); + + if ($isMySQL) { + $minor = $version === '10.2' ? 7 : 0; + $version = "{$version}.{$minor}-MariaDB"; + } + + $url .= '&serverVersion=' . $version; + } + + $envVars["{$prefix}URL"] = $url; + } + } + + return $envVars; + } + /** * @param array>> $relationships * diff --git a/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php b/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php index 9ff59dc..1d5d985 100644 --- a/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php +++ b/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php @@ -87,6 +87,7 @@ public function providerForTestLoadEnvVars(): iterable $routes = $this->createRoutes(); $expected = [ + 'DATABASE_URL' => 'mysql://user:some_password@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.4.0-MariaDB', 'CACHE_POOL' => 'cache.redis', 'CACHE_DSN' => 'rediscache.internal:6379?retry_interval=3', 'SESSION_HANDLER_ID' => NativeSessionHandler::class, @@ -110,6 +111,7 @@ public function providerForTestLoadEnvVars(): iterable ]; $expected = [ + 'DATABASE_URL' => 'mysql://user:some_password@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.4.0-MariaDB', 'CACHE_POOL' => 'cache.redis', 'CACHE_DSN' => 'rediscache.internal:6379?retry_interval=3', 'SESSION_HANDLER_ID' => NativeSessionHandler::class, @@ -130,11 +132,11 @@ public function providerForTestLoadEnvVars(): iterable ]; $expected = [ + 'DFS_DATABASE_URL' => 'mysql://dfs:dfs@localhost:3306/dfs', 'DFS_NFS_PATH' => '/mnt/dfs/nfs', 'DFS_DATABASE_CHARSET' => 'utf8mb4', 'DFS_DATABASE_COLLATION' => 'utf8mb4_unicode_520_ci', 'DFS_DATABASE_DRIVER' => 'pdo_mysql', - 'DFS_DATABASE_URL' => 'mysql://dfs:dfs@localhost:3306/dfs', 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', ]; @@ -144,6 +146,185 @@ public function providerForTestLoadEnvVars(): iterable $expected, $serverValues + ['PLATFORMSH_DFS_NFS_PATH' => '/mnt/dfs/nfs'], ]; + + // Database test cases ported from symfony-cli/envs/remote_test.go + + $expected = [ + 'DATABASE_URL' => 'postgres://main:main@database.internal:5432/main?sslmode=disable&charset=utf8', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ]; + + yield 'postgresql without version' => [ + [ + 'database' => [ + [ + 'username' => 'main', + 'password' => 'main', + 'host' => 'database.internal', + 'port' => 5432, + 'path' => 'main', + 'scheme' => 'pgsql', + 'query' => ['is_master' => true], + ], + ], + ], + $routes, + $expected, + $serverValues, + ]; + + $expected = [ + 'DATABASE_URL' => 'postgres://main:main@database.internal:5432/main?sslmode=disable&charset=utf8&serverVersion=9.6', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ]; + + yield 'postgresql with version 9.6' => [ + [ + 'database' => [ + [ + 'username' => 'main', + 'password' => 'main', + 'host' => 'database.internal', + 'port' => 5432, + 'path' => 'main', + 'scheme' => 'pgsql', + 'type' => 'postgresql:9.6', + 'query' => ['is_master' => true], + ], + ], + ], + $routes, + $expected, + $serverValues, + ]; + + $expected = [ + 'DATABASE_URL' => 'mysql://main:6e602888576703030f53c154051bd778@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.0.0-MariaDB', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ]; + + yield 'mysql with version 10.0' => [ + [ + 'database' => [ + [ + 'username' => 'main', + 'password' => '6e602888576703030f53c154051bd778', + 'host' => 'database.internal', + 'port' => 3306, + 'path' => 'main', + 'scheme' => 'mysql', + 'type' => 'mysql:10.0', + 'query' => ['is_master' => true], + ], + ], + ], + $routes, + $expected, + $serverValues, + ]; + + $expected = [ + 'DATABASE_URL' => 'postgres://main:main@database.internal:5432/main?sslmode=disable&charset=utf8&serverVersion=10', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ]; + + yield 'postgresql with version 10' => [ + [ + 'database' => [ + [ + 'username' => 'main', + 'password' => 'main', + 'host' => 'database.internal', + 'port' => 5432, + 'path' => 'main', + 'scheme' => 'pgsql', + 'type' => 'postgresql:10', + 'query' => ['is_master' => true], + ], + ], + ], + $routes, + $expected, + $serverValues, + ]; + + $expected = [ + 'DATABASE_URL' => 'mysql://database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.1.0-MariaDB', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ]; + + yield 'mysql without credentials version 10.1' => [ + [ + 'database' => [ + [ + 'host' => 'database.internal', + 'port' => 3306, + 'scheme' => 'mysql', + 'type' => 'mysql:10.1', + 'query' => [], + ], + ], + ], + $routes, + $expected, + $serverValues, + ]; + + $expected = [ + 'DATABASE_URL' => 'mysql://database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.2.7-MariaDB', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ]; + + yield 'mysql version 10.2 with special minor version' => [ + [ + 'database' => [ + [ + 'host' => 'database.internal', + 'port' => 3306, + 'scheme' => 'mysql', + 'type' => 'mysql:10.2', + 'query' => [], + ], + ], + ], + $routes, + $expected, + $serverValues, + ]; + + $expected = [ + 'DATABASE_URL' => 'mysql://user:pass@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.6.0-MariaDB', + 'DATABASE_1_URL' => 'mysql://replica_user:replica_pass@database-replica.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.6.0-MariaDB', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ]; + + yield 'two databases with indexed naming' => [ + [ + 'database' => [ + [ + 'username' => 'user', + 'password' => 'pass', + 'host' => 'database.internal', + 'port' => 3306, + 'scheme' => 'mysql', + 'type' => 'mysql:10.6', + 'query' => ['is_master' => true], + ], + [ + 'username' => 'replica_user', + 'password' => 'replica_pass', + 'host' => 'database-replica.internal', + 'port' => 3306, + 'scheme' => 'mysql', + 'type' => 'mysql:10.6', + 'query' => ['is_master' => false], + ], + ], + ], + $routes, + $expected, + $serverValues, + ]; } /** From 30dda5b316158da93e26556ebe5991e709bd0053 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Thu, 4 Dec 2025 10:13:59 +0100 Subject: [PATCH 11/20] IBX-10764: Update --- .../DependencyInjection/UpsunEnvVarLoader.php | 316 +++++++++++------- 1 file changed, 200 insertions(+), 116 deletions(-) diff --git a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php index 9bffd3a..edde2f0 100644 --- a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php +++ b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php @@ -9,8 +9,8 @@ namespace Ibexa\Bundle\Cloud\DependencyInjection; use Ibexa\Bundle\Core\Session\Handler\NativeSessionHandler; -use JsonException; use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; +use function is_string; final class UpsunEnvVarLoader implements EnvVarLoaderInterface { @@ -20,15 +20,17 @@ final class UpsunEnvVarLoader implements EnvVarLoaderInterface private const string DEFAULT_DATABASE_COLLATION = 'utf8mb4_unicode_520_ci'; + private const string DEFAULT_DFS_RELATIONSHIP_KEY = 'dfs_database'; + public function loadEnvVars(): array { - $relationshipsEncoded = $_SERVER['PLATFORM_RELATIONSHIPS'] ?? null; - $routesEncoded = $_SERVER['PLATFORM_ROUTES'] ?? null; - - if ($relationshipsEncoded === null || $routesEncoded === null) { + if (!isset($_SERVER['PLATFORM_RELATIONSHIPS']) || !isset($_SERVER['PLATFORM_ROUTES'])) { return []; } + $relationshipsEncoded = $_SERVER['PLATFORM_RELATIONSHIPS']; + $routesEncoded = $_SERVER['PLATFORM_ROUTES']; + $relationships = $this->decodePayload($relationshipsEncoded); $routes = $this->decodePayload($routesEncoded); @@ -36,13 +38,15 @@ public function loadEnvVars(): array return []; } + $groupedRelationships = $this->groupRelationshipsByScheme($relationships); + return array_filter( array_merge( - $this->buildDatabaseEnvVars($relationships), - $this->buildDfsEnvVars($relationships), - $this->buildCacheEnvVars($relationships), - $this->buildSessionEnvVars($relationships), - $this->buildSearchEnvVars($relationships), + $this->buildDatabaseEnvVars($groupedRelationships), + $this->buildDfsEnvVars($groupedRelationships), + $this->buildCacheEnvVars($groupedRelationships), + $this->buildSessionEnvVars($groupedRelationships), + $this->buildSearchEnvVars($groupedRelationships), $this->buildVarnishEnvVars($routes), ), static fn (string|int|null $value): bool => $value !== null && $value !== '' @@ -50,7 +54,7 @@ public function loadEnvVars(): array } /** - * @param array>> $relationships + * @param array> $relationships * * @return array */ @@ -58,64 +62,68 @@ private function buildDatabaseEnvVars(array $relationships): array { $envVars = []; - foreach ($relationships as $key => $allValues) { - // Uppercase the key: "database" -> "DATABASE", "database_site" -> "DATABASE_SITE" - $key = strtoupper($key); + foreach ($relationships as $scheme => $relationshipsByKey) { + $isPGSQL = $scheme === 'pgsql'; + $isMySQL = $scheme === 'mysql'; + if (!$isPGSQL && !$isMySQL) { + continue; + } - foreach ($allValues as $i => $endpoint) { - $scheme = $endpoint['scheme'] ?? ''; - $isPGSQL = str_starts_with($scheme, 'pgsql'); - $isMySQL = str_starts_with($scheme, 'mysql'); + $normalizedScheme = $isPGSQL ? 'postgres' : $scheme; - if (!$isPGSQL && !$isMySQL) { - continue; - } + foreach ($relationshipsByKey as $key => $endpoints) { + $key = strtoupper($key); - // Build prefix: first endpoint gets "DATABASE_", second gets "DATABASE_1_", etc. - $prefix = $i === 0 ? "{$key}_" : "{$key}_{$i}_"; - // Replace hyphens with underscores - $prefix = str_replace('-', '_', $prefix); + foreach ($endpoints as $i => $endpoint) { + $prefix = $i === 0 ? "{$key}_" : "{$key}_{$i}_"; + $prefix = str_replace('-', '_', $prefix); - // Normalize scheme for PostgreSQL - if ($isPGSQL) { - $scheme = 'postgres'; - } + $username = $endpoint['username'] ?? ''; + $password = $endpoint['password'] ?? ''; + $host = $endpoint['host'] ?? ''; + $port = $endpoint['port'] ?? 0; + $path = $endpoint['path'] ?? 'main'; - $username = $endpoint['username'] ?? ''; - $password = $endpoint['password'] ?? ''; - $host = $endpoint['host'] ?? ''; - $port = $endpoint['port'] ?? 0; - $path = $endpoint['path'] ?? 'main'; - - // Build URL - $url = sprintf('%s://', $scheme); - if ($username !== '') { - $url .= $username; - if ($password !== '') { - $url .= ':' . $password; + $url = sprintf('%s://', $normalizedScheme); + if ($username !== '') { + $url .= $username; + if ($password !== '') { + $url .= ':' . $password; + } + $url .= '@'; } - $url .= '@'; - } - $url .= sprintf('%s:%s/%s?sslmode=disable', $host, $port, $path); + $url .= sprintf('%s:%s/%s?sslmode=disable', $host, $port, $path); + + $charset = $isMySQL ? self::MYSQL_DEFAULT_DATABASE_CHARSET : self::PGSQL_DEFAULT_DATABASE_CHARSET; + $url .= '&charset=' . $charset; - // Add charset - $charset = $isMySQL ? self::MYSQL_DEFAULT_DATABASE_CHARSET : self::PGSQL_DEFAULT_DATABASE_CHARSET; - $url .= '&charset=' . $charset; + $type = $endpoint['type'] ?? null; + if ($type !== null && str_contains((string) $type, ':')) { + [, $version] = explode(':', (string) $type, 2); - // Add serverVersion for PHP/Doctrine - $type = $endpoint['type'] ?? null; - if ($type !== null && str_contains((string) $type, ':')) { - [, $version] = explode(':', (string) $type, 2); + if ($isMySQL) { + $minor = $version === '10.2' ? 7 : 0; + $version = "{$version}.{$minor}-MariaDB"; + } - if ($isMySQL) { - $minor = $version === '10.2' ? 7 : 0; - $version = "{$version}.{$minor}-MariaDB"; + $url .= '&serverVersion=' . $version; } - $url .= '&serverVersion=' . $version; - } + $envVars["{$prefix}URL"] = $url; + $envVars["{$prefix}USER"] = $username; + $envVars["{$prefix}USERNAME"] = $username; + $envVars["{$prefix}PASSWORD"] = $password; + $envVars["{$prefix}HOST"] = $host; + $envVars["{$prefix}PORT"] = (string) $port; + $envVars["{$prefix}NAME"] = $path; + $envVars["{$prefix}DATABASE"] = $path; + $envVars["{$prefix}DRIVER"] = $normalizedScheme; + $envVars["{$prefix}SERVER"] = sprintf('%s://%s:%s', $normalizedScheme, $host, $port); + + if ($key === strtoupper(self::DEFAULT_DFS_RELATIONSHIP_KEY)) { - $envVars["{$prefix}URL"] = $url; + } + } } } @@ -151,7 +159,7 @@ private function buildDfsEnvVars(array $relationships): array $pdoDriver = $this->normalizePdoDriver((string) ($endpoint['scheme'] ?? '')); $envVars[$this->envKey('dfs_database_driver')] = $pdoDriver; - // If driver is PGSQL, charset has to be set to utf8 + // If the driver is PGSQL, charset has to be set to utf8 if ($pdoDriver === 'pdo_pgsql') { $envVars[$this->envKey('dfs_database_charset')] = self::PGSQL_DEFAULT_DATABASE_CHARSET; } @@ -185,39 +193,68 @@ private function buildDfsEnvVars(array $relationships): array */ private function buildCacheEnvVars(array $relationships): array { - if (isset($relationships['rediscache'])) { - foreach ($relationships['rediscache'] as $endpoint) { - if (($endpoint['scheme'] ?? null) !== 'redis') { - continue; + $envVars = []; + $cachePoolSet = false; + + foreach ($relationships as $scheme => $relationshipsByKey) { + if ($scheme === 'redis') { + foreach ($relationshipsByKey as $key => $endpoints) { + $key = strtoupper($key); + + foreach ($endpoints as $i => $endpoint) { + $prefix = $i === 0 ? "{$key}_" : "{$key}_{$i}_"; + $prefix = str_replace('-', '_', $prefix); + + $host = $endpoint['host'] ?? ''; + $port = $endpoint['port'] ?? 0; + + $envVars["{$prefix}URL"] = sprintf('redis://%s:%s', $host, $port); + $envVars["{$prefix}HOST"] = $host; + $envVars["{$prefix}PORT"] = (string) $port; + $envVars["{$prefix}SCHEME"] = 'redis'; + + // Set cache_pool and cache_dsn for the first redis endpoint found + if (!$cachePoolSet) { + $envVars[$this->envKey('cache_pool')] = 'cache.redis'; + $envVars[$this->envKey('cache_dsn')] = sprintf( + '%s:%d?retry_interval=3', + $host, + $port, + ); + $cachePoolSet = true; + } + } } - - return [ - $this->envKey('cache_pool') => 'cache.redis', - $this->envKey('cache_dsn') => sprintf( - '%s:%d?retry_interval=3', - $endpoint['host'], - $endpoint['port'], - ), - ]; } - } - if (isset($relationships['cache'])) { - foreach ($relationships['cache'] as $endpoint) { - if (($endpoint['scheme'] ?? null) !== 'memcached') { - continue; - } + if ($scheme === 'memcached') { + foreach ($relationshipsByKey as $key => $endpoints) { + $key = strtoupper($key); - @trigger_error('Usage of Memcached is deprecated, redis is recommended', E_USER_DEPRECATED); + foreach ($endpoints as $i => $endpoint) { + $prefix = $i === 0 ? "{$key}_" : "{$key}_{$i}_"; + $prefix = str_replace('-', '_', $prefix); - return [ - $this->envKey('cache_pool') => 'cache.memcached', - $this->envKey('cache_dsn') => sprintf('%s:%d', $endpoint['host'], $endpoint['port']), - ]; + $host = $endpoint['host'] ?? ''; + $port = $endpoint['port'] ?? 0; + + $envVars["{$prefix}HOST"] = $host; + $envVars["{$prefix}PORT"] = (string) $port; + + // Set cache_pool and cache_dsn for the first memcached endpoint found (only if redis wasn't found) + if (!$cachePoolSet) { + @trigger_error('Usage of Memcached is deprecated, redis is recommended', E_USER_DEPRECATED); + + $envVars[$this->envKey('cache_pool')] = 'cache.memcached'; + $envVars[$this->envKey('cache_dsn')] = sprintf('%s:%d', $host, $port); + $cachePoolSet = true; + } + } + } } } - return []; + return $envVars; } /** @@ -260,38 +297,76 @@ private function buildSearchEnvVars(array $relationships): array $envVars = []; if (isset($relationships['solr'])) { - foreach ($relationships['solr'] as $endpoint) { - if (($endpoint['scheme'] ?? null) !== 'solr') { - continue; - } + foreach ($relationships['solr'] as $key => $endpoints) { + $key = strtoupper($key); - $envVars[$this->envKey('search_engine')] = 'solr'; - $envVars[$this->envKey('solr_dsn')] = sprintf( - 'http://%s:%d/%s', - $endpoint['host'], - $endpoint['port'], - 'solr' - ); - $envVars[$this->envKey('solr_core')] = substr((string) $endpoint['path'], 5); + foreach ($endpoints as $i => $endpoint) { + if (($endpoint['scheme'] ?? null) !== 'solr') { + continue; + } + + $prefix = $i === 0 ? "{$key}_" : "{$key}_{$i}_"; + $prefix = str_replace('-', '_', $prefix); + + $host = $endpoint['host'] ?? ''; + $port = $endpoint['port'] ?? 0; + $path = $endpoint['path'] ?? ''; + + $envVars[$this->envKey('search_engine')] = 'solr'; + $envVars[$this->envKey('solr_dsn')] = sprintf( + 'http://%s:%d/%s', + $host, + $port, + 'solr' + ); + $envVars[$this->envKey('solr_core')] = substr($path, 5); + + $envVars["{$prefix}HOST"] = $host; + $envVars["{$prefix}PORT"] = (string) $port; + $envVars["{$prefix}NAME"] = $path; + $envVars["{$prefix}DATABASE"] = $path; + } } } if (isset($relationships['elasticsearch'])) { - foreach ($relationships['elasticsearch'] as $endpoint) { - $dsn = sprintf('%s:%d', $endpoint['host'], $endpoint['port']); + foreach ($relationships['elasticsearch'] as $key => $endpoints) { + $key = strtoupper($key); - if (($endpoint['username'] ?? null) !== null && ($endpoint['password'] ?? null) !== null) { - $dsn = $endpoint['username'] . ':' . $endpoint['password'] . '@' . $dsn; - } + foreach ($endpoints as $i => $endpoint) { + $prefix = $i === 0 ? "{$key}_" : "{$key}_{$i}_"; + $prefix = str_replace('-', '_', $prefix); - if (($endpoint['path'] ?? null) !== null) { - $dsn .= '/' . ltrim((string) $endpoint['path'], '/'); - } + $host = $endpoint['host'] ?? ''; + $port = $endpoint['port'] ?? 0; + $scheme = $endpoint['scheme'] ?? 'http'; + $path = $endpoint['path'] ?? null; + + $dsn = sprintf('%s:%d', $host, $port); + + if (($endpoint['username'] ?? null) !== null && ($endpoint['password'] ?? null) !== null) { + $dsn = $endpoint['username'] . ':' . $endpoint['password'] . '@' . $dsn; + } + + if ($path !== null) { + $dsn .= '/' . ltrim((string) $path, '/'); + } - $dsn = $endpoint['scheme'] . '://' . $dsn; + $url = $scheme . '://' . $host . ':' . $port; + if ($path !== null && $path !== '') { + $url .= $path; + } + + $dsn = $scheme . '://' . $dsn; - $envVars[$this->envKey('search_engine')] = 'elasticsearch'; - $envVars[$this->envKey('elasticsearch_dsn')] = $dsn; + $envVars[$this->envKey('search_engine')] = 'elasticsearch'; + $envVars[$this->envKey('elasticsearch_dsn')] = $dsn; + + $envVars["{$prefix}URL"] = $url; + $envVars["{$prefix}HOST"] = $host; + $envVars["{$prefix}PORT"] = (string) $port; + $envVars["{$prefix}SCHEME"] = $scheme; + } } } @@ -328,7 +403,7 @@ private function buildVarnishEnvVars(array $routes): array if ($username !== null && $password !== null) { $domain = parse_url($purgeServer, PHP_URL_HOST); - if (\is_string($domain) && $domain !== '') { + if (is_string($domain) && $domain !== '') { $credentials = rawurlencode($username) . ':' . rawurlencode($password); $purgeServer = str_replace($domain, $credentials . '@' . $domain, $purgeServer); } @@ -351,15 +426,24 @@ private function buildVarnishEnvVars(array $routes): array private function decodePayload(string $payload): ?array { $decoded = base64_decode($payload, true); - if ($decoded === false) { - return null; - } - try { - return json_decode($decoded, true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException) { - return null; + return $decoded === false ? null : json_decode($decoded, true, JSON_THROW_ON_ERROR); + } + + private function groupRelationshipsByScheme(array $relationships): array + { + $groupedRelationships = []; + foreach ($relationships as $key => $endpoints) { + foreach ($endpoints as $endpoint) { + if (!isset($endpoint['scheme'])) { + continue; + } + + $groupedRelationships[$endpoint['scheme']][$key][] = $endpoint; + } } + + return $groupedRelationships; } private function normalizePdoDriver(string $scheme): string @@ -384,7 +468,7 @@ private function guessRepositoryDriver(): ?string } $scheme = parse_url($databaseUrl, PHP_URL_SCHEME); - if (!\is_string($scheme) || $scheme === '') { + if (!is_string($scheme) || $scheme === '') { return null; } @@ -409,6 +493,6 @@ private function getFirstNonEmptyEnv(string $name): ?string $value = $_SERVER[$name] ?? $_ENV[$name] ?? null; $value = $value === '' ? null : $value; - return \is_string($value) ? $value : null; + return is_string($value) ? $value : null; } } From 669073a80db7a40c83126a2a301e11af70a70346 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Sat, 6 Dec 2025 21:32:06 +0100 Subject: [PATCH 12/20] IBX-10764: Update loader --- .../DependencyInjection/UpsunEnvVarLoader.php | 326 +++++++++--------- 1 file changed, 162 insertions(+), 164 deletions(-) diff --git a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php index edde2f0..13e8168 100644 --- a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php +++ b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php @@ -9,8 +9,9 @@ namespace Ibexa\Bundle\Cloud\DependencyInjection; use Ibexa\Bundle\Core\Session\Handler\NativeSessionHandler; -use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; use function is_string; +use JsonException; +use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; final class UpsunEnvVarLoader implements EnvVarLoaderInterface { @@ -20,8 +21,6 @@ final class UpsunEnvVarLoader implements EnvVarLoaderInterface private const string DEFAULT_DATABASE_COLLATION = 'utf8mb4_unicode_520_ci'; - private const string DEFAULT_DFS_RELATIONSHIP_KEY = 'dfs_database'; - public function loadEnvVars(): array { if (!isset($_SERVER['PLATFORM_RELATIONSHIPS']) || !isset($_SERVER['PLATFORM_ROUTES'])) { @@ -45,8 +44,8 @@ public function loadEnvVars(): array $this->buildDatabaseEnvVars($groupedRelationships), $this->buildDfsEnvVars($groupedRelationships), $this->buildCacheEnvVars($groupedRelationships), - $this->buildSessionEnvVars($groupedRelationships), - $this->buildSearchEnvVars($groupedRelationships), + $this->buildSessionEnvVars($relationships, $groupedRelationships), + $this->buildSearchEnvVars($relationships), $this->buildVarnishEnvVars($routes), ), static fn (string|int|null $value): bool => $value !== null && $value !== '' @@ -54,29 +53,28 @@ public function loadEnvVars(): array } /** - * @param array> $relationships + * @param array>>> $groupedRelationships * * @return array */ - private function buildDatabaseEnvVars(array $relationships): array + private function buildDatabaseEnvVars(array $groupedRelationships): array { $envVars = []; - foreach ($relationships as $scheme => $relationshipsByKey) { - $isPGSQL = $scheme === 'pgsql'; - $isMySQL = $scheme === 'mysql'; + foreach ($groupedRelationships as $scheme => $relationshipsByKey) { + $isPGSQL = str_starts_with($scheme, 'pgsql'); + $isMySQL = str_starts_with($scheme, 'mysql'); if (!$isPGSQL && !$isMySQL) { continue; } - $normalizedScheme = $isPGSQL ? 'postgres' : $scheme; + $normalizedScheme = $isPGSQL ? 'postgres' : 'mysql'; foreach ($relationshipsByKey as $key => $endpoints) { $key = strtoupper($key); - foreach ($endpoints as $i => $endpoint) { - $prefix = $i === 0 ? "{$key}_" : "{$key}_{$i}_"; - $prefix = str_replace('-', '_', $prefix); + foreach (array_values($endpoints) as $i => $endpoint) { + $prefix = $this->buildPrefix($key, $i); $username = $endpoint['username'] ?? ''; $password = $endpoint['password'] ?? ''; @@ -86,9 +84,9 @@ private function buildDatabaseEnvVars(array $relationships): array $url = sprintf('%s://', $normalizedScheme); if ($username !== '') { - $url .= $username; + $url .= rawurlencode($username); if ($password !== '') { - $url .= ':' . $password; + $url .= ':' . rawurlencode($password); } $url .= '@'; } @@ -119,10 +117,6 @@ private function buildDatabaseEnvVars(array $relationships): array $envVars["{$prefix}DATABASE"] = $path; $envVars["{$prefix}DRIVER"] = $normalizedScheme; $envVars["{$prefix}SERVER"] = sprintf('%s://%s:%s', $normalizedScheme, $host, $port); - - if ($key === strtoupper(self::DEFAULT_DFS_RELATIONSHIP_KEY)) { - - } } } } @@ -131,11 +125,14 @@ private function buildDatabaseEnvVars(array $relationships): array } /** - * @param array>> $relationships + * Builds DFS-specific env vars. Note: DFS_DATABASE_URL and other standard database + * vars are already generated by buildDatabaseEnvVars - this only adds DFS-specific ones. + * + * @param array>>> $groupedRelationships * * @return array */ - private function buildDfsEnvVars(array $relationships): array + private function buildDfsEnvVars(array $groupedRelationships): array { $dfsPath = $_SERVER['PLATFORMSH_DFS_NFS_PATH'] ?? null; if ($dfsPath === null) { @@ -144,111 +141,114 @@ private function buildDfsEnvVars(array $relationships): array $envVars = [ $this->envKey('dfs_nfs_path') => $dfsPath, - $this->envKey('dfs_database_charset') => $_SERVER['DATABASE_CHARSET'] - ?? self::MYSQL_DEFAULT_DATABASE_CHARSET, - $this->envKey('dfs_database_collation') => $_SERVER['DATABASE_COLLATION'] - ?? self::DEFAULT_DATABASE_COLLATION, ]; - if (isset($relationships['dfs_database'])) { - foreach ($relationships['dfs_database'] as $endpoint) { - if (!isset($endpoint['query']['is_master'])) { - continue; - } + // Look for 'dfs_database' relationship within mysql/pgsql schemes to determine driver and charset + $dfsEndpoint = $this->findDfsEndpoint($groupedRelationships); - $pdoDriver = $this->normalizePdoDriver((string) ($endpoint['scheme'] ?? '')); - $envVars[$this->envKey('dfs_database_driver')] = $pdoDriver; + if ($dfsEndpoint !== null) { + $scheme = (string) ($dfsEndpoint['scheme'] ?? ''); + $isPgsql = str_starts_with($scheme, 'pgsql'); - // If the driver is PGSQL, charset has to be set to utf8 - if ($pdoDriver === 'pdo_pgsql') { - $envVars[$this->envKey('dfs_database_charset')] = self::PGSQL_DEFAULT_DATABASE_CHARSET; - } + $envVars[$this->envKey('dfs_database_driver')] = $this->normalizePdoDriver($scheme); + $envVars[$this->envKey('dfs_database_charset')] = $isPgsql + ? self::PGSQL_DEFAULT_DATABASE_CHARSET + : self::MYSQL_DEFAULT_DATABASE_CHARSET; - $envVars[$this->envKey('dfs_database_url')] = sprintf( - '%s://%s:%s@%s:%d/%s', - $endpoint['scheme'], - $endpoint['username'], - $endpoint['password'], - $endpoint['host'], - $endpoint['port'], - ltrim((string) $endpoint['path'], '/') - ); + // Collation is MySQL-specific, PostgreSQL doesn't use it + if (!$isPgsql) { + $envVars[$this->envKey('dfs_database_collation')] = self::DEFAULT_DATABASE_COLLATION; + } + } - break; + return $envVars; + } + + /** + * @param array>>> $groupedRelationships + * + * @return array|null + */ + private function findDfsEndpoint(array $groupedRelationships): ?array + { + foreach ($groupedRelationships as $scheme => $relationshipsByKey) { + if (!str_starts_with($scheme, 'mysql') && !str_starts_with($scheme, 'pgsql')) { + continue; } - } else { - $driver = $this->guessRepositoryDriver(); - if ($driver !== null) { - $envVars[$this->envKey('dfs_database_driver')] = $driver; + + if (!isset($relationshipsByKey['dfs_database'])) { + continue; + } + + foreach ($relationshipsByKey['dfs_database'] as $endpoint) { + if (isset($endpoint['query']['is_master']) && $endpoint['query']['is_master'] === true) { + return $endpoint; + } } } - return $envVars; + return null; } /** - * @param array>> $relationships + * @param array>>> $groupedRelationships Grouped by scheme * * @return array */ - private function buildCacheEnvVars(array $relationships): array + private function buildCacheEnvVars(array $groupedRelationships): array { $envVars = []; $cachePoolSet = false; - foreach ($relationships as $scheme => $relationshipsByKey) { - if ($scheme === 'redis') { - foreach ($relationshipsByKey as $key => $endpoints) { - $key = strtoupper($key); - - foreach ($endpoints as $i => $endpoint) { - $prefix = $i === 0 ? "{$key}_" : "{$key}_{$i}_"; - $prefix = str_replace('-', '_', $prefix); - - $host = $endpoint['host'] ?? ''; - $port = $endpoint['port'] ?? 0; - - $envVars["{$prefix}URL"] = sprintf('redis://%s:%s', $host, $port); - $envVars["{$prefix}HOST"] = $host; - $envVars["{$prefix}PORT"] = (string) $port; - $envVars["{$prefix}SCHEME"] = 'redis'; - - // Set cache_pool and cache_dsn for the first redis endpoint found - if (!$cachePoolSet) { - $envVars[$this->envKey('cache_pool')] = 'cache.redis'; - $envVars[$this->envKey('cache_dsn')] = sprintf( - '%s:%d?retry_interval=3', - $host, - $port, - ); - $cachePoolSet = true; - } + // Process Redis first (always preferred over memcached) + if (isset($groupedRelationships['redis'])) { + foreach ($groupedRelationships['redis'] as $key => $endpoints) { + $key = strtoupper($key); + + foreach (array_values($endpoints) as $i => $endpoint) { + $prefix = $this->buildPrefix($key, $i); + + $host = $endpoint['host'] ?? ''; + $port = $endpoint['port'] ?? 0; + + $envVars["{$prefix}URL"] = sprintf('redis://%s:%s', $host, $port); + $envVars["{$prefix}HOST"] = $host; + $envVars["{$prefix}PORT"] = (string) $port; + $envVars["{$prefix}SCHEME"] = 'redis'; + + if (!$cachePoolSet) { + $envVars[$this->envKey('cache_pool')] = 'cache.redis'; + $envVars[$this->envKey('cache_dsn')] = sprintf( + '%s:%d?retry_interval=3', + $host, + $port, + ); + $cachePoolSet = true; } } } + } - if ($scheme === 'memcached') { - foreach ($relationshipsByKey as $key => $endpoints) { - $key = strtoupper($key); + // Process Memcached (fallback, only sets cache_pool if redis wasn't found) + if (isset($groupedRelationships['memcached'])) { + foreach ($groupedRelationships['memcached'] as $key => $endpoints) { + $key = strtoupper($key); - foreach ($endpoints as $i => $endpoint) { - $prefix = $i === 0 ? "{$key}_" : "{$key}_{$i}_"; - $prefix = str_replace('-', '_', $prefix); + foreach (array_values($endpoints) as $i => $endpoint) { + $prefix = $this->buildPrefix($key, $i); - $host = $endpoint['host'] ?? ''; - $port = $endpoint['port'] ?? 0; + $host = $endpoint['host'] ?? ''; + $port = $endpoint['port'] ?? 0; - $envVars["{$prefix}HOST"] = $host; - $envVars["{$prefix}PORT"] = (string) $port; + $envVars["{$prefix}HOST"] = $host; + $envVars["{$prefix}PORT"] = (string) $port; - // Set cache_pool and cache_dsn for the first memcached endpoint found (only if redis wasn't found) - if (!$cachePoolSet) { - @trigger_error('Usage of Memcached is deprecated, redis is recommended', E_USER_DEPRECATED); + if (!$cachePoolSet) { + @trigger_error('Usage of Memcached is deprecated, redis is recommended', E_USER_DEPRECATED); - $envVars[$this->envKey('cache_pool')] = 'cache.memcached'; - $envVars[$this->envKey('cache_dsn')] = sprintf('%s:%d', $host, $port); - $cachePoolSet = true; - } + $envVars[$this->envKey('cache_pool')] = 'cache.memcached'; + $envVars[$this->envKey('cache_dsn')] = sprintf('%s:%d', $host, $port); + $cachePoolSet = true; } } } @@ -258,37 +258,50 @@ private function buildCacheEnvVars(array $relationships): array } /** - * @param array>> $relationships + * Uses Redis-based sessions if possible. If a dedicated 'redissession' relationship + * is available, use that. If not, fallback to any available Redis instance. + * + * @param array>> $relationships Original relationships + * @param array>>> $groupedRelationships Grouped by scheme * * @return array */ - private function buildSessionEnvVars(array $relationships): array + private function buildSessionEnvVars(array $relationships, array $groupedRelationships): array { - $endpoints = $relationships['redissession'] ?? $relationships['rediscache'] ?? null; - if ($endpoints === null) { - return []; - } + // First, try a dedicated 'redissession' relationship + if (isset($relationships['redissession'])) { + foreach ($relationships['redissession'] as $endpoint) { + if (($endpoint['scheme'] ?? null) !== 'redis') { + continue; + } - foreach ($endpoints as $endpoint) { - if (($endpoint['scheme'] ?? null) !== 'redis') { - continue; + return [ + $this->envKey('session_handler_id') => NativeSessionHandler::class, + $this->envKey('session_save_path') => sprintf('%s:%d', $endpoint['host'], $endpoint['port']), + ]; } + } - return [ - $this->envKey('session_handler_id') => NativeSessionHandler::class, - $this->envKey('session_save_path') => sprintf( - '%s:%d', - $endpoint['host'], - $endpoint['port'], - ), - ]; + // Fallback: use any available Redis instance (by scheme) + if (isset($groupedRelationships['redis'])) { + foreach ($groupedRelationships['redis'] as $endpoints) { + foreach ($endpoints as $endpoint) { + return [ + $this->envKey('session_handler_id') => NativeSessionHandler::class, + $this->envKey('session_save_path') => sprintf('%s:%d', $endpoint['host'], $endpoint['port']), + ]; + } + } } return []; } /** - * @param array>> $relationships + * Builds search engine env vars. + * Uses original relationships (not grouped) to check 'rel' field for elasticsearch. + * + * @param array>> $relationships Original relationships * * @return array */ @@ -296,17 +309,16 @@ private function buildSearchEnvVars(array $relationships): array { $envVars = []; - if (isset($relationships['solr'])) { - foreach ($relationships['solr'] as $key => $endpoints) { - $key = strtoupper($key); + foreach ($relationships as $key => $endpoints) { + $upperKey = strtoupper($key); - foreach ($endpoints as $i => $endpoint) { - if (($endpoint['scheme'] ?? null) !== 'solr') { - continue; - } + foreach (array_values($endpoints) as $i => $endpoint) { + $scheme = $endpoint['scheme'] ?? null; + $rel = $endpoint['rel'] ?? null; - $prefix = $i === 0 ? "{$key}_" : "{$key}_{$i}_"; - $prefix = str_replace('-', '_', $prefix); + // Handle Solr (by scheme) + if ($scheme === 'solr') { + $prefix = $this->buildPrefix($upperKey, $i); $host = $endpoint['host'] ?? ''; $port = $endpoint['port'] ?? 0; @@ -319,23 +331,17 @@ private function buildSearchEnvVars(array $relationships): array $port, 'solr' ); - $envVars[$this->envKey('solr_core')] = substr($path, 5); + $envVars[$this->envKey('solr_core')] = basename($path); $envVars["{$prefix}HOST"] = $host; $envVars["{$prefix}PORT"] = (string) $port; $envVars["{$prefix}NAME"] = $path; $envVars["{$prefix}DATABASE"] = $path; } - } - } - if (isset($relationships['elasticsearch'])) { - foreach ($relationships['elasticsearch'] as $key => $endpoints) { - $key = strtoupper($key); - - foreach ($endpoints as $i => $endpoint) { - $prefix = $i === 0 ? "{$key}_" : "{$key}_{$i}_"; - $prefix = str_replace('-', '_', $prefix); + // Handle Elasticsearch (by rel field, like Go version) + if ($rel === 'elasticsearch') { + $prefix = $this->buildPrefix($upperKey, $i); $host = $endpoint['host'] ?? ''; $port = $endpoint['port'] ?? 0; @@ -345,7 +351,7 @@ private function buildSearchEnvVars(array $relationships): array $dsn = sprintf('%s:%d', $host, $port); if (($endpoint['username'] ?? null) !== null && ($endpoint['password'] ?? null) !== null) { - $dsn = $endpoint['username'] . ':' . $endpoint['password'] . '@' . $dsn; + $dsn = rawurlencode($endpoint['username']) . ':' . rawurlencode($endpoint['password']) . '@' . $dsn; } if ($path !== null) { @@ -427,9 +433,22 @@ private function decodePayload(string $payload): ?array { $decoded = base64_decode($payload, true); - return $decoded === false ? null : json_decode($decoded, true, JSON_THROW_ON_ERROR); + if ($decoded === false) { + return null; + } + + try { + return json_decode($decoded, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException) { + return null; + } } + /** + * @param array>> $relationships + * + * @return array>>> + */ private function groupRelationshipsByScheme(array $relationships): array { $groupedRelationships = []; @@ -455,29 +474,16 @@ private function normalizePdoDriver(string $scheme): string return str_starts_with($scheme, 'pdo_') ? $scheme : 'pdo_' . $scheme; } - private function guessRepositoryDriver(): ?string + private function envKey(string $parameterName): string { - $explicit = $this->getFirstNonEmptyEnv('DATABASE_DRIVER'); - if ($explicit !== null) { - return $explicit; - } - - $databaseUrl = $this->getFirstNonEmptyEnv('DATABASE_URL'); - if ($databaseUrl === null) { - return null; - } - - $scheme = parse_url($databaseUrl, PHP_URL_SCHEME); - if (!is_string($scheme) || $scheme === '') { - return null; - } - - return $this->normalizePdoDriver($scheme); + return strtoupper(str_replace(['.', '-'], '_', $parameterName)); } - private function envKey(string $parameterName): string + private function buildPrefix(string $key, int $index): string { - return strtoupper(str_replace(['.', '-'], '_', $parameterName)); + $prefix = $index === 0 ? "{$key}_" : "{$key}_{$index}_"; + + return str_replace('-', '_', $prefix); } /** @@ -487,12 +493,4 @@ private function isVarnishRoute(array $route): bool { return ($route['type'] ?? null) === 'upstream' && ($route['upstream'] ?? null) === 'varnish'; } - - private function getFirstNonEmptyEnv(string $name): ?string - { - $value = $_SERVER[$name] ?? $_ENV[$name] ?? null; - $value = $value === '' ? null : $value; - - return is_string($value) ? $value : null; - } } From 999982cbe57b02562c9d643014670815bfbef7a4 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 8 Dec 2025 10:10:25 +0100 Subject: [PATCH 13/20] IBX-10764: Updated tests --- .../TrustedHeaderClientIpEventSubscriber.php | 1 + .../UpsunEnvVarLoaderTest.php | 629 +++++++++--------- 2 files changed, 320 insertions(+), 310 deletions(-) diff --git a/src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php b/src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php index 4f4dcb8..2b34075 100644 --- a/src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php +++ b/src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php @@ -54,6 +54,7 @@ public function onKernelRequest(RequestEvent $event): void $request->headers->set('X_FORWARDED_FOR', $trustedClientIp); } + /** @var int<0, 63> $trustedHeaderSet */ Request::setTrustedProxies($trustedProxies, $trustedHeaderSet); } diff --git a/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php b/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php index 1d5d985..d1355a9 100644 --- a/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php +++ b/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php @@ -76,87 +76,243 @@ public function testLoadEnvVars( */ public function providerForTestLoadEnvVars(): iterable { - $relationships = [ - 'database' => [ - $this->createDatabase(), - ], - 'rediscache' => [ - $this->createRedisCache(), - ], - ]; $routes = $this->createRoutes(); - - $expected = [ - 'DATABASE_URL' => 'mysql://user:some_password@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.4.0-MariaDB', - 'CACHE_POOL' => 'cache.redis', - 'CACHE_DSN' => 'rediscache.internal:6379?retry_interval=3', - 'SESSION_HANDLER_ID' => NativeSessionHandler::class, - 'SESSION_SAVE_PATH' => 'rediscache.internal:6379', - 'SEARCH_ENGINE' => 'elasticsearch', - 'ELASTICSEARCH_DSN' => 'http://elasticsearch.internal:9200', - 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', - ]; - - $serverValues = [ - 'PLATFORM_PROJECT_ENTROPY' => 'project_entropy', - ]; + $serverValues = ['PLATFORM_PROJECT_ENTROPY' => 'project_entropy']; yield 'redis cache with session fallback and elasticsearch' => [ - $relationships + ['elasticsearch' => [ - $this->createElasticSearch(), - ]], + [ + 'replica_db' => [ + [ + 'host' => 'database.internal', + 'hostname' => 'mysql_db_random._.eu-4.platformsh.site', + 'cluster' => 'some_cluster', + 'service' => 'mysqldb', + 'rel' => 'user', + 'scheme' => 'mysql', + 'username' => 'user', + 'password' => 'some_password', + 'port' => 3306, + 'epoch' => 0, + 'path' => 'main', + 'query' => ['is_master' => true], + 'fragment' => null, + 'public' => false, + 'host_mapped' => false, + 'type' => 'mariadb:10.4', + 'instance_ips' => ['127.0.0.1'], + 'ip' => '127.0.0.1', + ], + ], + 'rediscache' => [ + [ + 'host' => 'rediscache.internal', + 'hostname' => 'redis.service._.eu-4.platformsh.site', + 'cluster' => 'some_cluster', + 'service' => 'rediscache', + 'rel' => 'redis', + 'scheme' => 'redis', + 'username' => null, + 'password' => null, + 'port' => 6379, + 'epoch' => 0, + 'path' => null, + 'query' => [], + 'fragment' => null, + 'public' => false, + 'host_mapped' => false, + 'type' => 'redis:5.0', + 'instance_ips' => ['127.0.0.1'], + 'ip' => '127.0.0.1', + ], + ], + 'site_elasticsearch' => [ + [ + 'username' => null, + 'scheme' => 'http', + 'service' => 'elasticsearch', + 'fragment' => null, + 'ip' => '123.456.78.90', + 'hostname' => 'azertyuiopqsdfghjklm.elasticsearch.service._.eu-1.platformsh.site', + 'port' => 9200, + 'cluster' => 'azertyuiopqsdf-main-7rqtwti', + 'host' => 'elasticsearch.internal', + 'rel' => 'elasticsearch', + 'path' => null, + 'query' => [], + 'password' => 'ChangeMe', + 'type' => 'elasticsearch:8.5', + 'public' => false, + 'host_mapped' => false, + ], + ], + ], $routes, - $expected, + [ + 'REPLICA_DB_URL' => 'mysql://user:some_password@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.4.0-MariaDB', + 'REPLICA_DB_USER' => 'user', + 'REPLICA_DB_USERNAME' => 'user', + 'REPLICA_DB_PASSWORD' => 'some_password', + 'REPLICA_DB_HOST' => 'database.internal', + 'REPLICA_DB_PORT' => '3306', + 'REPLICA_DB_NAME' => 'main', + 'REPLICA_DB_DATABASE' => 'main', + 'REPLICA_DB_DRIVER' => 'mysql', + 'REPLICA_DB_SERVER' => 'mysql://database.internal:3306', + 'REDISCACHE_URL' => 'redis://rediscache.internal:6379', + 'REDISCACHE_HOST' => 'rediscache.internal', + 'REDISCACHE_PORT' => '6379', + 'REDISCACHE_SCHEME' => 'redis', + 'CACHE_POOL' => 'cache.redis', + 'CACHE_DSN' => 'rediscache.internal:6379?retry_interval=3', + 'SESSION_HANDLER_ID' => NativeSessionHandler::class, + 'SESSION_SAVE_PATH' => 'rediscache.internal:6379', + 'SEARCH_ENGINE' => 'elasticsearch', + 'ELASTICSEARCH_DSN' => 'http://elasticsearch.internal:9200', + 'SITE_ELASTICSEARCH_URL' => 'http://elasticsearch.internal:9200', + 'SITE_ELASTICSEARCH_HOST' => 'elasticsearch.internal', + 'SITE_ELASTICSEARCH_PORT' => '9200', + 'SITE_ELASTICSEARCH_SCHEME' => 'http', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ], $serverValues, ]; - $expected = [ - 'DATABASE_URL' => 'mysql://user:some_password@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.4.0-MariaDB', - 'CACHE_POOL' => 'cache.redis', - 'CACHE_DSN' => 'rediscache.internal:6379?retry_interval=3', - 'SESSION_HANDLER_ID' => NativeSessionHandler::class, - 'SESSION_SAVE_PATH' => 'rediscache.internal:6379', - 'SEARCH_ENGINE' => 'solr', - 'SOLR_DSN' => 'http://solr.internal:8080/solr', - 'SOLR_CORE' => 'collection1', - 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', - ]; - yield 'redis cache with session fallback and solr' => [ - $relationships + ['solr' => [ - $this->createSolr(), - ]], + [ + 'replica_db' => [ + [ + 'host' => 'database.internal', + 'hostname' => 'mysql_db_random._.eu-4.platformsh.site', + 'cluster' => 'some_cluster', + 'service' => 'mysqldb', + 'rel' => 'user', + 'scheme' => 'mysql', + 'username' => 'user', + 'password' => 'some_password', + 'port' => 3306, + 'epoch' => 0, + 'path' => 'main', + 'query' => ['is_master' => true], + 'fragment' => null, + 'public' => false, + 'host_mapped' => false, + 'type' => 'mariadb:10.4', + 'instance_ips' => ['127.0.0.1'], + 'ip' => '127.0.0.1', + ], + ], + 'rediscache' => [ + [ + 'host' => 'rediscache.internal', + 'hostname' => 'redis.service._.eu-4.platformsh.site', + 'cluster' => 'some_cluster', + 'service' => 'rediscache', + 'rel' => 'redis', + 'scheme' => 'redis', + 'username' => null, + 'password' => null, + 'port' => 6379, + 'epoch' => 0, + 'path' => null, + 'query' => [], + 'fragment' => null, + 'public' => false, + 'host_mapped' => false, + 'type' => 'redis:5.0', + 'instance_ips' => ['127.0.0.1'], + 'ip' => '127.0.0.1', + ], + ], + 'site_solr' => [ + [ + 'username' => null, + 'scheme' => 'solr', + 'service' => 'solr', + 'fragment' => null, + 'ip' => '123.456.78.90', + 'hostname' => 'host.solr.service._.eu-1.platformsh.site', + 'port' => 8080, + 'cluster' => 'some-cluster', + 'host' => 'solr.internal', + 'rel' => 'solr', + 'path' => 'solr/collection1', + 'query' => [], + 'password' => null, + 'type' => 'solr:9.9', + 'public' => false, + 'host_mapped' => false, + ], + ], + ], $routes, - $expected, + [ + 'REPLICA_DB_URL' => 'mysql://user:some_password@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.4.0-MariaDB', + 'REPLICA_DB_USER' => 'user', + 'REPLICA_DB_USERNAME' => 'user', + 'REPLICA_DB_PASSWORD' => 'some_password', + 'REPLICA_DB_HOST' => 'database.internal', + 'REPLICA_DB_PORT' => '3306', + 'REPLICA_DB_NAME' => 'main', + 'REPLICA_DB_DATABASE' => 'main', + 'REPLICA_DB_DRIVER' => 'mysql', + 'REPLICA_DB_SERVER' => 'mysql://database.internal:3306', + 'REDISCACHE_URL' => 'redis://rediscache.internal:6379', + 'REDISCACHE_HOST' => 'rediscache.internal', + 'REDISCACHE_PORT' => '6379', + 'REDISCACHE_SCHEME' => 'redis', + 'CACHE_POOL' => 'cache.redis', + 'CACHE_DSN' => 'rediscache.internal:6379?retry_interval=3', + 'SESSION_HANDLER_ID' => NativeSessionHandler::class, + 'SESSION_SAVE_PATH' => 'rediscache.internal:6379', + 'SEARCH_ENGINE' => 'solr', + 'SOLR_DSN' => 'http://solr.internal:8080/solr', + 'SOLR_CORE' => 'collection1', + 'SITE_SOLR_HOST' => 'solr.internal', + 'SITE_SOLR_PORT' => '8080', + 'SITE_SOLR_NAME' => 'solr/collection1', + 'SITE_SOLR_DATABASE' => 'solr/collection1', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ], $serverValues, ]; - $expected = [ - 'DFS_DATABASE_URL' => 'mysql://dfs:dfs@localhost:3306/dfs', - 'DFS_NFS_PATH' => '/mnt/dfs/nfs', - 'DFS_DATABASE_CHARSET' => 'utf8mb4', - 'DFS_DATABASE_COLLATION' => 'utf8mb4_unicode_520_ci', - 'DFS_DATABASE_DRIVER' => 'pdo_mysql', - 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', - ]; - yield 'dfs' => [ - ['dfs_database' => [$this->createDfs()]], + [ + 'dfs_database' => [ + [ + 'host' => 'dfs_database.internal', + 'scheme' => 'mysql', + 'username' => 'dfs', + 'password' => 'dfs', + 'port' => 3306, + 'path' => 'dfs', + 'query' => ['is_master' => true], + ], + ], + ], $routes, - $expected, + [ + 'DFS_DATABASE_URL' => 'mysql://dfs:dfs@dfs_database.internal:3306/dfs?sslmode=disable&charset=utf8mb4', + 'DFS_DATABASE_USER' => 'dfs', + 'DFS_DATABASE_USERNAME' => 'dfs', + 'DFS_DATABASE_PASSWORD' => 'dfs', + 'DFS_DATABASE_HOST' => 'dfs_database.internal', + 'DFS_DATABASE_PORT' => '3306', + 'DFS_DATABASE_NAME' => 'dfs', + 'DFS_DATABASE_DATABASE' => 'dfs', + 'DFS_DATABASE_DRIVER' => 'pdo_mysql', + 'DFS_DATABASE_SERVER' => 'mysql://dfs_database.internal:3306', + 'DFS_NFS_PATH' => '/mnt/dfs/nfs', + 'DFS_DATABASE_CHARSET' => 'utf8mb4', + 'DFS_DATABASE_COLLATION' => 'utf8mb4_unicode_520_ci', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ], $serverValues + ['PLATFORMSH_DFS_NFS_PATH' => '/mnt/dfs/nfs'], ]; - // Database test cases ported from symfony-cli/envs/remote_test.go - - $expected = [ - 'DATABASE_URL' => 'postgres://main:main@database.internal:5432/main?sslmode=disable&charset=utf8', - 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', - ]; - yield 'postgresql without version' => [ [ - 'database' => [ + 'pg_main' => [ [ 'username' => 'main', 'password' => 'main', @@ -169,18 +325,25 @@ public function providerForTestLoadEnvVars(): iterable ], ], $routes, - $expected, + [ + 'PG_MAIN_URL' => 'postgres://main:main@database.internal:5432/main?sslmode=disable&charset=utf8', + 'PG_MAIN_USER' => 'main', + 'PG_MAIN_USERNAME' => 'main', + 'PG_MAIN_PASSWORD' => 'main', + 'PG_MAIN_HOST' => 'database.internal', + 'PG_MAIN_PORT' => '5432', + 'PG_MAIN_NAME' => 'main', + 'PG_MAIN_DATABASE' => 'main', + 'PG_MAIN_DRIVER' => 'postgres', + 'PG_MAIN_SERVER' => 'postgres://database.internal:5432', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ], $serverValues, ]; - $expected = [ - 'DATABASE_URL' => 'postgres://main:main@database.internal:5432/main?sslmode=disable&charset=utf8&serverVersion=9.6', - 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', - ]; - yield 'postgresql with version 9.6' => [ [ - 'database' => [ + 'legacy_pgsql' => [ [ 'username' => 'main', 'password' => 'main', @@ -194,18 +357,25 @@ public function providerForTestLoadEnvVars(): iterable ], ], $routes, - $expected, + [ + 'LEGACY_PGSQL_URL' => 'postgres://main:main@database.internal:5432/main?sslmode=disable&charset=utf8&serverVersion=9.6', + 'LEGACY_PGSQL_USER' => 'main', + 'LEGACY_PGSQL_USERNAME' => 'main', + 'LEGACY_PGSQL_PASSWORD' => 'main', + 'LEGACY_PGSQL_HOST' => 'database.internal', + 'LEGACY_PGSQL_PORT' => '5432', + 'LEGACY_PGSQL_NAME' => 'main', + 'LEGACY_PGSQL_DATABASE' => 'main', + 'LEGACY_PGSQL_DRIVER' => 'postgres', + 'LEGACY_PGSQL_SERVER' => 'postgres://database.internal:5432', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ], $serverValues, ]; - $expected = [ - 'DATABASE_URL' => 'mysql://main:6e602888576703030f53c154051bd778@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.0.0-MariaDB', - 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', - ]; - yield 'mysql with version 10.0' => [ [ - 'database' => [ + 'app_db' => [ [ 'username' => 'main', 'password' => '6e602888576703030f53c154051bd778', @@ -219,18 +389,25 @@ public function providerForTestLoadEnvVars(): iterable ], ], $routes, - $expected, + [ + 'APP_DB_URL' => 'mysql://main:6e602888576703030f53c154051bd778@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.0.0-MariaDB', + 'APP_DB_USER' => 'main', + 'APP_DB_USERNAME' => 'main', + 'APP_DB_PASSWORD' => '6e602888576703030f53c154051bd778', + 'APP_DB_HOST' => 'database.internal', + 'APP_DB_PORT' => '3306', + 'APP_DB_NAME' => 'main', + 'APP_DB_DATABASE' => 'main', + 'APP_DB_DRIVER' => 'mysql', + 'APP_DB_SERVER' => 'mysql://database.internal:3306', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ], $serverValues, ]; - $expected = [ - 'DATABASE_URL' => 'postgres://main:main@database.internal:5432/main?sslmode=disable&charset=utf8&serverVersion=10', - 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', - ]; - yield 'postgresql with version 10' => [ [ - 'database' => [ + 'analytics_db' => [ [ 'username' => 'main', 'password' => 'main', @@ -244,18 +421,25 @@ public function providerForTestLoadEnvVars(): iterable ], ], $routes, - $expected, + [ + 'ANALYTICS_DB_URL' => 'postgres://main:main@database.internal:5432/main?sslmode=disable&charset=utf8&serverVersion=10', + 'ANALYTICS_DB_USER' => 'main', + 'ANALYTICS_DB_USERNAME' => 'main', + 'ANALYTICS_DB_PASSWORD' => 'main', + 'ANALYTICS_DB_HOST' => 'database.internal', + 'ANALYTICS_DB_PORT' => '5432', + 'ANALYTICS_DB_NAME' => 'main', + 'ANALYTICS_DB_DATABASE' => 'main', + 'ANALYTICS_DB_DRIVER' => 'postgres', + 'ANALYTICS_DB_SERVER' => 'postgres://database.internal:5432', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ], $serverValues, ]; - $expected = [ - 'DATABASE_URL' => 'mysql://database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.1.0-MariaDB', - 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', - ]; - yield 'mysql without credentials version 10.1' => [ [ - 'database' => [ + 'cache_db' => [ [ 'host' => 'database.internal', 'port' => 3306, @@ -266,18 +450,22 @@ public function providerForTestLoadEnvVars(): iterable ], ], $routes, - $expected, + [ + 'CACHE_DB_URL' => 'mysql://database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.1.0-MariaDB', + 'CACHE_DB_HOST' => 'database.internal', + 'CACHE_DB_PORT' => '3306', + 'CACHE_DB_NAME' => 'main', + 'CACHE_DB_DATABASE' => 'main', + 'CACHE_DB_DRIVER' => 'mysql', + 'CACHE_DB_SERVER' => 'mysql://database.internal:3306', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ], $serverValues, ]; - $expected = [ - 'DATABASE_URL' => 'mysql://database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.2.7-MariaDB', - 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', - ]; - yield 'mysql version 10.2 with special minor version' => [ [ - 'database' => [ + 'cms_database' => [ [ 'host' => 'database.internal', 'port' => 3306, @@ -288,19 +476,22 @@ public function providerForTestLoadEnvVars(): iterable ], ], $routes, - $expected, + [ + 'CMS_DATABASE_URL' => 'mysql://database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.2.7-MariaDB', + 'CMS_DATABASE_HOST' => 'database.internal', + 'CMS_DATABASE_PORT' => '3306', + 'CMS_DATABASE_NAME' => 'main', + 'CMS_DATABASE_DATABASE' => 'main', + 'CMS_DATABASE_DRIVER' => 'mysql', + 'CMS_DATABASE_SERVER' => 'mysql://database.internal:3306', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ], $serverValues, ]; - $expected = [ - 'DATABASE_URL' => 'mysql://user:pass@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.6.0-MariaDB', - 'DATABASE_1_URL' => 'mysql://replica_user:replica_pass@database-replica.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.6.0-MariaDB', - 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', - ]; - yield 'two databases with indexed naming' => [ [ - 'database' => [ + 'main_mysql' => [ [ 'username' => 'user', 'password' => 'pass', @@ -322,7 +513,29 @@ public function providerForTestLoadEnvVars(): iterable ], ], $routes, - $expected, + [ + 'MAIN_MYSQL_URL' => 'mysql://user:pass@database.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.6.0-MariaDB', + 'MAIN_MYSQL_USER' => 'user', + 'MAIN_MYSQL_USERNAME' => 'user', + 'MAIN_MYSQL_PASSWORD' => 'pass', + 'MAIN_MYSQL_HOST' => 'database.internal', + 'MAIN_MYSQL_PORT' => '3306', + 'MAIN_MYSQL_NAME' => 'main', + 'MAIN_MYSQL_DATABASE' => 'main', + 'MAIN_MYSQL_DRIVER' => 'mysql', + 'MAIN_MYSQL_SERVER' => 'mysql://database.internal:3306', + 'MAIN_MYSQL_1_URL' => 'mysql://replica_user:replica_pass@database-replica.internal:3306/main?sslmode=disable&charset=utf8mb4&serverVersion=10.6.0-MariaDB', + 'MAIN_MYSQL_1_USER' => 'replica_user', + 'MAIN_MYSQL_1_USERNAME' => 'replica_user', + 'MAIN_MYSQL_1_PASSWORD' => 'replica_pass', + 'MAIN_MYSQL_1_HOST' => 'database-replica.internal', + 'MAIN_MYSQL_1_PORT' => '3306', + 'MAIN_MYSQL_1_NAME' => 'main', + 'MAIN_MYSQL_1_DATABASE' => 'main', + 'MAIN_MYSQL_1_DRIVER' => 'mysql', + 'MAIN_MYSQL_1_SERVER' => 'mysql://database-replica.internal:3306', + 'HTTPCACHE_VARNISH_INVALIDATE_TOKEN' => 'project_entropy', + ], $serverValues, ]; } @@ -381,208 +594,4 @@ private function createRoutes(): array ], ]; } - - /** - * @return array{ - * host: string, - * hostname: string, - * cluster: string, - * service: string, - * rel: string, - * scheme: string, - * username: string, - * password: string, - * port: int, - * epoch: int, - * path: string, - * query: array, - * fragment: null, - * public: bool, - * host_mapped: bool, - * type: string, - * instance_ips: array, - * ip: string - * } - */ - private function createDatabase(): array - { - return [ - 'host' => 'database.internal', - 'hostname' => 'mysql_db_random._.eu-4.platformsh.site', - 'cluster' => 'some_cluster', - 'service' => 'mysqldb', - 'rel' => 'user', - 'scheme' => 'mysql', - 'username' => 'user', - 'password' => 'some_password', - 'port' => 3306, - 'epoch' => 0, - 'path' => 'main', - 'query' => ['is_master' => true], - 'fragment' => null, - 'public' => false, - 'host_mapped' => false, - 'type' => 'mariadb:10.4', - 'instance_ips' => ['127.0.0.1'], - 'ip' => '127.0.0.1', - ]; - } - - /** - * @return array{ - * host: string, - * hostname: string, - * cluster: string, - * service: string, - * rel: string, - * scheme: string, - * username: null, - * password: null, - * port: int, - * epoch: int, - * path: null, - * query: array, - * fragment: null, - * public: bool, - * host_mapped: bool, - * type: string, - * instance_ips: array, - * ip: string - * } - */ - private function createRedisCache(): array - { - return [ - 'host' => 'rediscache.internal', - 'hostname' => 'redis.service._.eu-4.platformsh.site', - 'cluster' => 'some_cluster', - 'service' => 'rediscache', - 'rel' => 'redis', - 'scheme' => 'redis', - 'username' => null, - 'password' => null, - 'port' => 6379, - 'epoch' => 0, - 'path' => null, - 'query' => [], - 'fragment' => null, - 'public' => false, - 'host_mapped' => false, - 'type' => 'redis:5.0', - 'instance_ips' => ['127.0.0.1'], - 'ip' => '127.0.0.1', - ]; - } - - /** - * @return array{ - * username: null, - * scheme: string, - * service: string, - * fragment: null, - * ip: string, - * hostname: string, - * port: int, - * cluster: string, - * host: string, - * rel: string, - * path: null, - * query: array, - * password: string, - * type: string, - * public: bool, - * host_mapped: bool - * } - */ - private function createElasticSearch(): array - { - return [ - 'username' => null, - 'scheme' => 'http', - 'service' => 'elasticsearch', - 'fragment' => null, - 'ip' => '123.456.78.90', - 'hostname' => 'azertyuiopqsdfghjklm.elasticsearch.service._.eu-1.platformsh.site', - 'port' => 9200, - 'cluster' => 'azertyuiopqsdf-main-7rqtwti', - 'host' => 'elasticsearch.internal', - 'rel' => 'elasticsearch', - 'path' => null, - 'query' => [], - 'password' => 'ChangeMe', - 'type' => 'elasticsearch:8.5', - 'public' => false, - 'host_mapped' => false, - ]; - } - - /** - * @return array{ - * username: null, - * scheme: string, - * service: string, - * fragment: null, - * ip: string, - * hostname: string, - * port: int, - * cluster: string, - * host: string, - * rel: string, - * path: string, - * query: array, - * password: null, - * type: string, - * public: bool, - * host_mapped: bool - * } - */ - private function createSolr(): array - { - return [ - 'username' => null, - 'scheme' => 'solr', - 'service' => 'solr', - 'fragment' => null, - 'ip' => '123.456.78.90', - 'hostname' => 'host.solr.service._.eu-1.platformsh.site', - 'port' => 8080, - 'cluster' => 'some-cluster', - 'host' => 'solr.internal', - 'rel' => 'solr', - 'path' => 'solr/collection1', - 'query' => [], - 'password' => null, - 'type' => 'solr:9.9', - 'public' => false, - 'host_mapped' => false, - ]; - } - - /** - * @return array{ - * host: string, - * scheme: string, - * username: string, - * password: string, - * port: int, - * path: string, - * query: array{is_master: bool} - * } - */ - private function createDfs(): array - { - $parts = parse_url('mysql://dfs:dfs@localhost:3306/dfs'); - - return [ - 'host' => $parts['host'], - 'scheme' => $parts['scheme'], - 'username' => $parts['user'], - 'password' => $parts['pass'], - 'port' => $parts['port'], - 'path' => ltrim($parts['path'], '/'), - 'query' => [ - 'is_master' => true, - ], - ]; - } } From 89e59740fd63fe87bdd3cc68bf7df6f5f74a3fe9 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 8 Dec 2025 10:26:28 +0100 Subject: [PATCH 14/20] IBX-10764: Cleared PHPStan baseline --- phpstan-baseline.neon | 7 ------- 1 file changed, 7 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index cf8431e..e69de29 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,7 +0,0 @@ -parameters: - ignoreErrors: - - - message: '#^Parameter \#2 \$trustedHeaderSet of static method Symfony\\Component\\HttpFoundation\\Request\:\:setTrustedProxies\(\) expects int\<0, 63\>, int given\.$#' - identifier: argument.type - count: 1 - path: src/bundle/EventSubscriber/TrustedHeaderClientIpEventSubscriber.php From 299c84b265bbd6e64ea4b6a4febbbd09ab7d346c Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 8 Dec 2025 10:28:04 +0100 Subject: [PATCH 15/20] IBX-10764: CS --- src/bundle/Resources/config/services/console.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundle/Resources/config/services/console.yaml b/src/bundle/Resources/config/services/console.yaml index 0c20987..efecbe1 100644 --- a/src/bundle/Resources/config/services/console.yaml +++ b/src/bundle/Resources/config/services/console.yaml @@ -3,4 +3,4 @@ services: autowire: true autoconfigure: true - Ibexa\Cloud\Command\IbexaSetupCommand: ~ \ No newline at end of file + Ibexa\Cloud\Command\IbexaSetupCommand: ~ From 313bd2b177712a6a27738074949f4fa2dbc3cecd Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 8 Dec 2025 10:34:52 +0100 Subject: [PATCH 16/20] IBX-10764: Update --- src/bundle/DependencyInjection/UpsunEnvVarLoader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php index 13e8168..631bb53 100644 --- a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php +++ b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php @@ -339,7 +339,7 @@ private function buildSearchEnvVars(array $relationships): array $envVars["{$prefix}DATABASE"] = $path; } - // Handle Elasticsearch (by rel field, like Go version) + // Handle Elasticsearch (by 'rel' field) if ($rel === 'elasticsearch') { $prefix = $this->buildPrefix($upperKey, $i); From 67bf0636c7ce798847d4e5065e00f4b84432d4ac Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 8 Dec 2025 10:35:37 +0100 Subject: [PATCH 17/20] IBX-10764: Update --- src/bundle/DependencyInjection/UpsunEnvVarLoader.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php index 631bb53..205df39 100644 --- a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php +++ b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php @@ -9,7 +9,6 @@ namespace Ibexa\Bundle\Cloud\DependencyInjection; use Ibexa\Bundle\Core\Session\Handler\NativeSessionHandler; -use function is_string; use JsonException; use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; From 71d77a967c25b64d9e5ba17c0f5774afb3ed3274 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 8 Dec 2025 12:07:19 +0100 Subject: [PATCH 18/20] IBX-10764: Updated test --- tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php b/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php index d1355a9..5da7fa9 100644 --- a/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php +++ b/tests/bundle/DependencyInjection/UpsunEnvVarLoaderTest.php @@ -128,18 +128,18 @@ public function providerForTestLoadEnvVars(): iterable 'site_elasticsearch' => [ [ 'username' => null, + 'password' => null, 'scheme' => 'http', 'service' => 'elasticsearch', 'fragment' => null, 'ip' => '123.456.78.90', - 'hostname' => 'azertyuiopqsdfghjklm.elasticsearch.service._.eu-1.platformsh.site', + 'hostname' => 'something.elasticsearch.service._.eu-1.platformsh.site', 'port' => 9200, - 'cluster' => 'azertyuiopqsdf-main-7rqtwti', + 'cluster' => 'something-main-7rqtwti', 'host' => 'elasticsearch.internal', 'rel' => 'elasticsearch', 'path' => null, 'query' => [], - 'password' => 'ChangeMe', 'type' => 'elasticsearch:8.5', 'public' => false, 'host_mapped' => false, From 1c2bbfcfc99132139a98c8d90385a5459baff211 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 8 Dec 2025 15:34:06 +0100 Subject: [PATCH 19/20] IBX-10764: Updated loader --- .../DependencyInjection/UpsunEnvVarLoader.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php index 205df39..3cb2af7 100644 --- a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php +++ b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php @@ -12,7 +12,7 @@ use JsonException; use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; -final class UpsunEnvVarLoader implements EnvVarLoaderInterface +final readonly class UpsunEnvVarLoader implements EnvVarLoaderInterface { private const string MYSQL_DEFAULT_DATABASE_CHARSET = 'utf8mb4'; @@ -282,14 +282,12 @@ private function buildSessionEnvVars(array $relationships, array $groupedRelatio } // Fallback: use any available Redis instance (by scheme) - if (isset($groupedRelationships['redis'])) { - foreach ($groupedRelationships['redis'] as $endpoints) { - foreach ($endpoints as $endpoint) { - return [ - $this->envKey('session_handler_id') => NativeSessionHandler::class, - $this->envKey('session_save_path') => sprintf('%s:%d', $endpoint['host'], $endpoint['port']), - ]; - } + foreach ($groupedRelationships['redis'] as $endpoints) { + foreach ($endpoints as $endpoint) { + return [ + $this->envKey('session_handler_id') => NativeSessionHandler::class, + $this->envKey('session_save_path') => sprintf('%s:%d', $endpoint['host'], $endpoint['port']), + ]; } } From 726d3f31f3167923263531192ed24b12121d653c Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 8 Dec 2025 15:43:47 +0100 Subject: [PATCH 20/20] IBX-10764: Rollback --- .../DependencyInjection/UpsunEnvVarLoader.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php index 3cb2af7..129892f 100644 --- a/src/bundle/DependencyInjection/UpsunEnvVarLoader.php +++ b/src/bundle/DependencyInjection/UpsunEnvVarLoader.php @@ -282,12 +282,14 @@ private function buildSessionEnvVars(array $relationships, array $groupedRelatio } // Fallback: use any available Redis instance (by scheme) - foreach ($groupedRelationships['redis'] as $endpoints) { - foreach ($endpoints as $endpoint) { - return [ - $this->envKey('session_handler_id') => NativeSessionHandler::class, - $this->envKey('session_save_path') => sprintf('%s:%d', $endpoint['host'], $endpoint['port']), - ]; + if (isset($groupedRelationships['redis'])) { + foreach ($groupedRelationships['redis'] as $endpoints) { + foreach ($endpoints as $endpoint) { + return [ + $this->envKey('session_handler_id') => NativeSessionHandler::class, + $this->envKey('session_save_path') => sprintf('%s:%d', $endpoint['host'], $endpoint['port']), + ]; + } } }