From fa2b4cbb79b6d9780f20e7bc746bf8516d959078 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Wed, 29 Oct 2025 09:37:46 +0100 Subject: [PATCH 1/6] 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 79e343cc726e2b52de6cae6c145fddde4559b8d3 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Thu, 30 Oct 2025 12:51:13 +0100 Subject: [PATCH 2/6] 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 f760a78f867fbab5dc7b6b4cb4c469078842754b Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Fri, 31 Oct 2025 10:12:07 +0100 Subject: [PATCH 3/6] 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 42a104cd8edc7e9e22ac885c61c3c3f9ae54ff8b Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Fri, 31 Oct 2025 10:14:39 +0100 Subject: [PATCH 4/6] 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 0a0c340c1d59178f73550265822fa8d196bf9dc0 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Mon, 3 Nov 2025 22:23:03 +0100 Subject: [PATCH 5/6] 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 b6b5d779a2a39b4fb432c73fba79f3e92d517cb0 Mon Sep 17 00:00:00 2001 From: Bartek Wajda Date: Wed, 5 Nov 2025 01:07:15 +0100 Subject: [PATCH 6/6] 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;