|
15 | 15 | use Magento\CloudDocker\Service\ServiceFactory; |
16 | 16 | use Magento\CloudDocker\Service\ServiceInterface; |
17 | 17 | use Symfony\Component\Yaml\Yaml; |
| 18 | +use Symfony\Component\Yaml\Tag\TaggedValue; |
| 19 | + |
18 | 20 | use Exception; |
19 | 21 |
|
20 | 22 | /** |
@@ -67,23 +69,35 @@ public function __construct( |
67 | 69 | $this->serviceFactory = $serviceFactory; |
68 | 70 | } |
69 | 71 |
|
70 | | - /** |
71 | | - * @inheritDoc |
72 | | - * @throws ConfigurationMismatchException |
73 | | - */ |
| 72 | + /** |
| 73 | + * @inheritDoc |
| 74 | + * |
| 75 | + * @SuppressWarnings("PMD.CyclomaticComplexity") |
| 76 | + * @SuppressWarnings("PMD.NPathComplexity") |
| 77 | + */ |
74 | 78 | public function read(): Repository |
75 | 79 | { |
76 | | - $appConfigFile = $this->fileList->getAppConfig(); |
| 80 | + $appConfigFile = $this->fileList->getAppConfig(); |
77 | 81 | $servicesConfigFile = $this->fileList->getServicesConfig(); |
78 | 82 |
|
79 | 83 | if (!$this->filesystem->exists($appConfigFile) || !$this->filesystem->exists($servicesConfigFile)) { |
80 | 84 | return new Repository(); |
81 | 85 | } |
82 | 86 |
|
83 | 87 | try { |
| 88 | + $flags = 0; |
| 89 | + if (defined(Yaml::class . '::PARSE_CONSTANT')) { |
| 90 | + $flags |= Yaml::PARSE_CONSTANT; |
| 91 | + } |
| 92 | + if (defined(Yaml::class . '::PARSE_CUSTOM_TAGS')) { |
| 93 | + $flags |= Yaml::PARSE_CUSTOM_TAGS; |
| 94 | + } |
84 | 95 | $appConfig = Yaml::parse( |
85 | | - $this->filesystem->get($this->fileList->getAppConfig()) |
| 96 | + $this->filesystem->get($this->fileList->getAppConfig()), |
| 97 | + $flags |
86 | 98 | ); |
| 99 | + |
| 100 | + $appConfig = $this->normalizeYamlData($appConfig); |
87 | 101 | } catch (\Exception $exception) { |
88 | 102 | throw new SourceException($exception->getMessage(), $exception->getCode(), $exception); |
89 | 103 | } |
@@ -148,9 +162,83 @@ public function read(): Repository |
148 | 162 | } |
149 | 163 |
|
150 | 164 | /** |
| 165 | + * Recursively normalizes Symfony YAML TaggedValue objects into PHP-native values. |
| 166 | + * |
| 167 | + * Handles the following YAML tags: |
| 168 | + * - !env: resolves environment variables. |
| 169 | + * - !include: parses and normalizes included YAML files. |
| 170 | + * - !php/const: resolves PHP constants (e.g. !php/const:\PDO::ATTR_ERRMODE). |
| 171 | + * - Other or unknown tags: recursively normalize their values. |
| 172 | + * |
| 173 | + * Ensures all YAML data is converted to scalars or arrays suitable for safe merging. |
| 174 | + * |
| 175 | + * @param mixed $data The parsed YAML data (array, scalar, or TaggedValue). |
| 176 | + * @return mixed The normalized data structure. |
| 177 | + * |
| 178 | + * @SuppressWarnings("PHPMD.NPathComplexity") |
| 179 | + * @SuppressWarnings("PHPMD.CyclomaticComplexity") Method is intentionally complex due to tag resolution logic. |
| 180 | + */ |
| 181 | + private function normalizeYamlData(mixed $data): mixed |
| 182 | + { |
| 183 | + if ($data instanceof TaggedValue) { |
| 184 | + $tag = $data->getTag(); // e.g. "php/const:\PDO::MYSQL_ATTR_LOCAL_INFILE" |
| 185 | + $value = $data->getValue(); |
| 186 | + |
| 187 | + // Handle php/const tags (Symfony strips leading '!') |
| 188 | + if (str_starts_with($tag, 'php/const:')) { |
| 189 | + // Extract the constant name |
| 190 | + $constName = substr($tag, strlen('php/const:')); |
| 191 | + $constName = ltrim($constName, '\\'); |
| 192 | + |
| 193 | + // Resolve the constant name to its value if defined |
| 194 | + $constKey = defined($constName) ? constant($constName) : $constName; |
| 195 | + |
| 196 | + // Handle YAML quirk where ": 1" is parsed literally |
| 197 | + $raw = is_string($value) ? $value : (string)$value; |
| 198 | + $cleanVal = str_replace([':', ' '], '', $raw); |
| 199 | + $constVal = is_numeric($cleanVal) ? (int)$cleanVal : $cleanVal; |
| 200 | + |
| 201 | + return [$constKey => $constVal]; |
| 202 | + } |
| 203 | + |
| 204 | + // Handle !env |
| 205 | + if ($tag === 'env') { |
| 206 | + $envValue = getenv((string)$value); |
| 207 | + return $envValue !== false ? $envValue : null; |
| 208 | + } |
| 209 | + |
| 210 | + // Handle !include |
| 211 | + if ($tag === 'include') { |
| 212 | + if (file_exists((string)$value)) { |
| 213 | + $included = Yaml::parseFile((string)$value); |
| 214 | + return $this->normalizeYamlData($included); |
| 215 | + } |
| 216 | + return null; |
| 217 | + } |
| 218 | + |
| 219 | + // Default — recursively normalize nested tagged structures |
| 220 | + $normalized = $this->normalizeYamlData($value); |
| 221 | + return is_array($normalized) ? $normalized : [$normalized]; |
| 222 | + } |
| 223 | + |
| 224 | + // Recursively normalize arrays |
| 225 | + if (is_array($data)) { |
| 226 | + foreach ($data as $key => $value) { |
| 227 | + $data[$key] = $this->normalizeYamlData($value); |
| 228 | + } |
| 229 | + } |
| 230 | + |
| 231 | + return $data; |
| 232 | + } |
| 233 | + |
| 234 | + /** |
| 235 | + * Adds service relationships to the repository. |
| 236 | + * |
151 | 237 | * @param Repository $repository |
152 | 238 | * @param array $relationships |
| 239 | + * |
153 | 240 | * @return Repository |
| 241 | + * |
154 | 242 | * @throws SourceException |
155 | 243 | */ |
156 | 244 | private function addRelationships(Repository $repository, array $relationships): Repository |
@@ -363,8 +451,16 @@ private function addMailhog(Repository $repository): Repository |
363 | 451 | private function getServiceConfig(): array |
364 | 452 | { |
365 | 453 | try { |
| 454 | + $flags = 0; |
| 455 | + if (defined(Yaml::class . '::PARSE_CONSTANT')) { |
| 456 | + $flags |= Yaml::PARSE_CONSTANT; |
| 457 | + } |
| 458 | + if (defined(Yaml::class . '::PARSE_CUSTOM_TAGS')) { |
| 459 | + $flags |= Yaml::PARSE_CUSTOM_TAGS; |
| 460 | + } |
366 | 461 | $servicesConfig = Yaml::parse( |
367 | | - $this->filesystem->get($this->fileList->getServicesConfig()) |
| 462 | + $this->filesystem->get($this->fileList->getServicesConfig()), |
| 463 | + $flags |
368 | 464 | ); |
369 | 465 | } catch (Exception $exception) { |
370 | 466 | throw new SourceException($exception->getMessage(), $exception->getCode(), $exception); |
|
0 commit comments