*/ protected array $sets = []; public function __construct( protected readonly SettingDefinitionValidation $settingDefinitionValidation, protected readonly YamlFileLoader $yamlFileLoader, ) {} /** * @return array */ public function getSetDefinitions(): array { return $this->sets; } public function addSet(SetDefinition $set): void { $this->sets[$set->name] = $set; } public function get(\SplFileInfo $fileInfo, ?string $virtualSetPath = null): SetDefinition { $filename = GeneralUtility::fixWindowsFilePath($fileInfo->getPathname()); $path = dirname($filename); $virtualSetPath ??= $path . '/'; // No placeholders or imports processed on purpose // Use dependencies for shared sets try { $set = Yaml::parseFile($filename); } catch (ParseException $e) { $source = $virtualSetPath . basename($filename); throw new InvalidSetException('Failed to parse set definition from "' . $source . '": ' . $e->getMessage(), 1711024370, $e); } $setName = $set['name'] ?? ''; $settingsDefinitionsFile = $path . '/settings.definitions.yaml'; if (is_file($settingsDefinitionsFile)) { try { $settingsDefinitions = Yaml::parseFile($settingsDefinitionsFile, Yaml::PARSE_OBJECT | Yaml::PARSE_OBJECT_FOR_MAP); } catch (ParseException $e) { $source = $virtualSetPath . basename($settingsDefinitionsFile); throw new InvalidSettingsDefinitionsException( 'Invalid settings definition. Source: ' . $source, 1711024374, $e, $setName ); } if (!is_object($settingsDefinitions->settings ?? null)) { $source = $virtualSetPath . basename($settingsDefinitionsFile); throw new InvalidSettingsDefinitionsException( 'Missing "settings" key in settings definitions. Source: ' . $source, 1711024378, null, $setName ); } // YAML maps are decoded as objects. Normalize them to arrays so // settings/category definitions can be handled uniformly below. $set['settingsDefinitions'] = array_map( static fn(?object $value): ?array => $value === null ? null : (array)$value, get_object_vars($settingsDefinitions->settings) ); $set['categoryDefinitions'] = []; if (isset($settingsDefinitions->categories)) { $set['categoryDefinitions'] = array_map( static fn(?object $value): ?array => $value === null ? null : (array)$value, get_object_vars($settingsDefinitions->categories) ); } } $settingsFile = $path . '/settings.yaml'; if (is_file($settingsFile)) { try { // HEADS UP: YamlFileLoader::PROCESS_PLACEHOLDERS is omitted on purpose and MUST NOT be added. // Site sets are intended to be self-contained and must not rely on implicit // dependencies to global (environment) variables. $settings = $this->yamlFileLoader->load($settingsFile, YamlFileLoader::PROCESS_IMPORTS | YamlFileLoader::ALLOW_EMPTY_FILE); } catch (YamlParseException $e) { $source = $virtualSetPath . basename($settingsFile); throw new InvalidSettingsException('Invalid settings format. Source: ' . $source, 1711024380, $e, $setName); } $set['settings'] = $settings; } $routeEnhancersFile = $path . '/route-enhancers.yaml'; if (is_file($routeEnhancersFile)) { try { $routeEnhancers = $this->yamlFileLoader->load($routeEnhancersFile, YamlFileLoader::PROCESS_IMPORTS | YamlFileLoader::ALLOW_EMPTY_FILE); } catch (YamlParseException $e) { $source = $virtualSetPath . basename($routeEnhancersFile); throw new InvalidSetRouteEnhancersException( 'Invalid route enhancers format. Source: ' . $source, 1764749081, $e, $setName ); } if ($routeEnhancers !== [] && !is_array($routeEnhancers['routeEnhancers'] ?? null)) { $source = $virtualSetPath . basename($routeEnhancersFile); throw new InvalidSetRouteEnhancersException( 'Missing "routeEnhancers" key in route enhancers file. Source: ' . $source, 1764749082, null, $setName ); } if ($routeEnhancers !== [] && array_keys($routeEnhancers) !== ['routeEnhancers']) { $source = $virtualSetPath . basename($routeEnhancersFile); throw new InvalidSetRouteEnhancersException( 'Superfluous keys in route enhancers file. Use "routeEnhancers" as root level key. Source: ' . $source, 1764749083, null, $setName ); } $set['routeEnhancers'] = $routeEnhancers['routeEnhancers'] ?? []; } if (($set['labels'] ?? '') === '') { if (is_file($path . '/labels.xlf')) { $set['labels'] = $virtualSetPath . 'labels.xlf'; } } return $this->createDefinition($set, $virtualSetPath); } protected function createDefinition(array $set, string $basePath): SetDefinition { $settingsDefinitions = []; $labels = $set['labels'] ?? null; unset($set['labels']); if ($labels) { $set['label'] ??= 'LLL:' . $labels . ':label'; } foreach (($set['settingsDefinitions'] ?? []) as $setting => $options) { // Cast objects to arrays if (is_object($options['options'] ?? null)) { $options['options'] = (array)$options['options']; } if (is_array($options['enum'] ?? null)) { $options['enum'] = array_combine( $options['enum'], array_map( static fn(string|int|float|bool $value): string => sprintf( '{label}:settings.%s.enum.%s', $setting, is_bool($value) ? ($value ? 'true' : 'false') : (string)$value ), $options['enum'] ) ); } elseif (is_object($options['enum'] ?? null)) { $options['enum'] = (array)$options['enum']; } if (is_array($options['enum'] ?? null)) { foreach ($options['enum'] as $enumValue => $enumLabel) { if ($enumLabel === null) { $options['enum'][$enumValue] = (string)$enumValue; } } } if (is_object($options['tags'] ?? null)) { $options['tags'] = array_values((array)$options['tags']); } if ($labels) { $domain = 'LLL:' . $labels . ':'; $options['label'] ??= $domain . 'settings.' . $setting; $options['description'] ??= $domain . 'settings.description.' . $setting; if (is_array($options['enum'] ?? null)) { foreach ($options['enum'] as $enumValue => $enumLabel) { if (is_string($enumLabel) && str_starts_with($enumLabel, '{label}:')) { $options['enum'][$enumValue] = $domain . substr($enumLabel, 8); } } } } $settingDefinitionData = [...['key' => $setting], ...$options]; try { $definition = new SettingDefinition(...$settingDefinitionData); } catch (\Error $e) { throw new InvalidSettingsDefinitionsException( 'Invalid setting definition "' . $setting . '": ' . json_encode($options) . ' – ' . $this->getObjectConstructionErrors($e, SettingDefinition::class, $settingDefinitionData), 1702623312, $e, $set['name'] ?? '' ); } try { $this->settingDefinitionValidation->validate($definition); } catch (InvalidSettingDefinitionException $e) { throw new InvalidSettingsDefinitionsException( $e->getMessage(), 1752483401, $e, $set['name'] ?? '' ); } $settingsDefinitions[] = $definition; } $categoryDefinitions = []; foreach (($set['categoryDefinitions'] ?? []) as $category => $options) { if ($labels) { $options['label'] ??= 'LLL:' . $labels . ':categories.' . $category; $options['description'] ??= 'LLL:' . $labels . ':categories.description.' . $category; } try { $definition = new CategoryDefinition(...[...['key' => $category], ...$options]); } catch (\Error $e) { throw new InvalidCategoryDefinitionsException( 'Invalid category definition "' . $category . '": ' . json_encode($options), 1702623313, $e, $set['name'] ?? '' ); } $categoryDefinitions[] = $definition; } foreach (($set['routeEnhancers'] ?? []) as $identifier => $config) { if (!is_array($config)) { throw new InvalidSetRouteEnhancersException( sprintf('Invalid route enhancer definition "%s": expected array, got %s', $identifier, gettype($config)), 1732800002, null, $set['name'] ?? '' ); } } $setData = [ ...$set, 'settingsDefinitions' => $settingsDefinitions, 'categoryDefinitions' => $categoryDefinitions, ]; $setData['typoscript'] ??= $basePath; $setData['pagets'] ??= $basePath . 'page.tsconfig'; try { return new SetDefinition(...$setData); } catch (\Error $e) { throw new InvalidSetException( 'Invalid set definition: ' . json_encode($set) . ' – ' . $this->getObjectConstructionErrors($e, SetDefinition::class, $setData), 1170859526, $e, $set['name'] ?? '' ); } } protected function getObjectConstructionErrors( \Error $error, string $className, array $arguments, ): string { $reflection = new \ReflectionClass($className); $constructor = $reflection->getConstructor(); $parameters = $constructor->getParameters(); $missingParameters = []; $typeErrors = []; foreach ($parameters as $parameter) { if (isset($arguments[$parameter->name])) { $value = $arguments[$parameter->name]; unset($arguments[$parameter->name]); $type = $parameter->getType(); if (!$this->typeMatches($type, $value)) { $typeErrors[$parameter->name] = (string)$type; } } elseif (!$parameter->isDefaultValueAvailable()) { $missingParameters[] = $parameter->name; } } $errors = []; if ($missingParameters !== []) { $errors[] = 'Missing properties: ' . implode(', ', $missingParameters); } if ($arguments !== []) { $errors[] = 'Invalid properties: ' . implode(', ', array_keys($arguments)); } if ($typeErrors !== []) { $errors[] = 'Invalid type: ' . implode(', ', array_keys($typeErrors)); } if ($errors === []) { return $error->getMessage(); } return implode('; ', $errors); } protected function typeMatches( \ReflectionType $type, mixed $value ): bool { if ($type->allowsNull() && $value === null) { return true; } if ($type instanceof \ReflectionUnionType) { foreach ($type->getTypes() as $t) { if ($this->typeMatches($t, $value)) { return true; } } return false; } if ($type instanceof \ReflectionIntersectionType) { foreach ($type->getTypes() as $t) { if (!$this->typeMatches($t, $value)) { return false; } } return true; } if ($type instanceof \ReflectionNamedType) { $typeName = $type->getName(); $valueType = gettype($value); if ($valueType === 'object') { return is_subclass_of($value, $typeName); } return $valueType === $typeName; } return true; } }