Files
cms-core/Classes/Site/Set/YamlSetDefinitionProvider.php

372 lines
14 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Core\Site\Set;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
use TYPO3\CMS\Core\Configuration\Loader\Exception\YamlParseException;
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
use TYPO3\CMS\Core\Settings\CategoryDefinition;
use TYPO3\CMS\Core\Settings\InvalidSettingDefinitionException;
use TYPO3\CMS\Core\Settings\SettingDefinition;
use TYPO3\CMS\Core\Settings\SettingDefinitionValidation;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal
*/
#[Autoconfigure(public: true)]
class YamlSetDefinitionProvider
{
/** @var array<string, SetDefinition> */
protected array $sets = [];
public function __construct(
protected readonly SettingDefinitionValidation $settingDefinitionValidation,
protected readonly YamlFileLoader $yamlFileLoader,
) {}
/**
* @return array<string, SetDefinition>
*/
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;
}
}