TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:15 +02:00
commit 6830982e7d
295 changed files with 31995 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
<?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\Extbase\Property;
use TYPO3\CMS\Extbase\Exception as ExtbaseException;
/**
* A generic Property related exception
*/
class Exception extends ExtbaseException {}
@@ -0,0 +1,25 @@
<?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\Extbase\Property\Exception;
use TYPO3\CMS\Extbase\Property\Exception;
/**
* A "Duplicate Object" Exception
*/
class DuplicateObjectException extends Exception {}
@@ -0,0 +1,25 @@
<?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\Extbase\Property\Exception;
use TYPO3\CMS\Extbase\Property\Exception;
/**
* A "Duplicate Type Converter" Exception
*/
class DuplicateTypeConverterException extends Exception {}
@@ -0,0 +1,25 @@
<?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\Extbase\Property\Exception;
use TYPO3\CMS\Extbase\Property\Exception;
/**
* An "Invalid Data Type" Exception
*/
class InvalidDataTypeException extends Exception {}
@@ -0,0 +1,25 @@
<?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\Extbase\Property\Exception;
use TYPO3\CMS\Extbase\Property\Exception;
/**
* An "Invalid Property Mapping Configuration" Exception
*/
class InvalidPropertyMappingConfigurationException extends Exception {}
@@ -0,0 +1,25 @@
<?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\Extbase\Property\Exception;
use TYPO3\CMS\Extbase\Property\Exception;
/**
* An "Invalid Source" Exception
*/
class InvalidSourceException extends Exception {}
@@ -0,0 +1,25 @@
<?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\Extbase\Property\Exception;
use TYPO3\CMS\Extbase\Property\Exception;
/**
* An "Invalid Target" Exception
*/
class InvalidTargetException extends Exception {}
@@ -0,0 +1,22 @@
<?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\Extbase\Property\Exception;
use TYPO3\CMS\Extbase\Property\Exception;
class InvalidTypeConverterConfigurationException extends Exception {}
@@ -0,0 +1,25 @@
<?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\Extbase\Property\Exception;
use TYPO3\CMS\Extbase\Property\Exception;
/**
* A "TargetNotFound" Exception
*/
class TargetNotFoundException extends Exception {}
@@ -0,0 +1,25 @@
<?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\Extbase\Property\Exception;
use TYPO3\CMS\Extbase\Property\Exception;
/**
* A "Type Converter" Exception
*/
class TypeConverterException extends Exception {}
+211
View File
@@ -0,0 +1,211 @@
<?php
/*
* 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\Extbase\Property;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Extbase\Error\Error;
use TYPO3\CMS\Extbase\Error\Result;
use TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException;
use TYPO3\CMS\Extbase\Property\Exception\TargetNotFoundException;
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
/**
* The Property Mapper transforms simple types (arrays, strings, integers, floats, booleans) to objects or other simple types.
* It is used most prominently to map incoming HTTP arguments to objects.
*/
#[Autoconfigure(public: true)]
class PropertyMapper
{
/**
* A list of property mapping messages (errors, warnings) which have occurred on last mapping.
*/
protected Result $messages;
public function __construct(
protected TypeConverterRegistry $typeConverterRegistry,
protected PropertyMappingConfigurationBuilder $configurationBuilder
) {
$this->resetMessages();
}
/**
* Map $source to $targetType, and return the result
*
* @param mixed $source the source data to map. MUST be a simple type, NO object allowed!
* @param string $targetType The type of the target; can be either a class name or a simple type.
* @param PropertyMappingConfigurationInterface|null $configuration Configuration for the property mapping. If NULL, the PropertyMappingConfigurationBuilder will create a default configuration.
* @return mixed an instance of $targetType
*/
public function convert(mixed $source, string $targetType, ?PropertyMappingConfigurationInterface $configuration = null): mixed
{
$configuration ??= $this->configurationBuilder->build();
$currentPropertyPath = [];
try {
$result = $this->doMapping($source, $targetType, $configuration, $currentPropertyPath);
if ($result instanceof Error) {
return null;
}
return $result;
} catch (TargetNotFoundException $e) {
throw $e;
} catch (\Exception $e) {
throw new Exception('Exception while property mapping at property path "' . implode('.', $currentPropertyPath) . '": ' . $e->getMessage(), 1297759968, $e);
}
}
/**
* Get the messages of the last Property Mapping.
*/
public function getMessages(): Result
{
return $this->messages;
}
/**
* Resets the messages of the last Property Mapping.
*/
public function resetMessages(): void
{
$this->messages = new Result();
}
/**
* Internal function which actually does the property mapping.
*
* @param mixed $source the source data to map. MUST be a simple type, NO object allowed!
* @param string $targetType The type of the target; can be either a class name or a simple type.
* @param PropertyMappingConfigurationInterface $configuration Configuration for the property mapping.
* @param array $currentPropertyPath The property path currently being mapped; used for knowing the context in case an exception is thrown.
* @return mixed an instance of $targetType
*
* @internal since TYPO3 v12.0
*/
protected function doMapping(mixed $source, string $targetType, PropertyMappingConfigurationInterface $configuration, array &$currentPropertyPath)
{
if (is_object($source)) {
$targetType = $this->parseCompositeType($targetType);
if ($source instanceof $targetType) {
return $source;
}
}
$source ??= '';
$typeConverter = $this->findTypeConverter($source, $targetType, $configuration);
$targetType = $typeConverter->getTargetTypeForSource($source, $targetType, $configuration);
$convertedChildProperties = [];
foreach ($typeConverter->getSourceChildPropertiesToBeConverted($source) as $sourcePropertyName => $sourcePropertyValue) {
$targetPropertyName = $configuration->getTargetPropertyName($sourcePropertyName);
if ($configuration->shouldSkip($targetPropertyName)) {
continue;
}
if (!$configuration->shouldMap($targetPropertyName)) {
if ($configuration->shouldSkipUnknownProperties()) {
continue;
}
throw new InvalidPropertyMappingConfigurationException('It is not allowed to map property "' . $targetPropertyName . '". You need to use $propertyMappingConfiguration->allowProperties(\'' . $targetPropertyName . '\') to enable mapping of this property.', 1355155913);
}
$targetPropertyType = $typeConverter->getTypeOfChildProperty($targetType, $targetPropertyName, $configuration);
$subConfiguration = $configuration->getConfigurationFor($targetPropertyName);
$currentPropertyPath[] = $targetPropertyName;
$targetPropertyValue = $this->doMapping($sourcePropertyValue, $targetPropertyType, $subConfiguration, $currentPropertyPath);
array_pop($currentPropertyPath);
if (!($targetPropertyValue instanceof Error)) {
$convertedChildProperties[$targetPropertyName] = $targetPropertyValue;
}
}
$result = $typeConverter->convertFrom($source, $targetType, $convertedChildProperties, $configuration);
if ($result instanceof Error) {
$this->messages->forProperty(implode('.', $currentPropertyPath))->addError($result);
}
return $result;
}
/**
* Determine the type converter to be used. If no converter has been found, an exception is raised.
*
* @return TypeConverterInterface Type Converter which should be used to convert between $source and $targetType.
*
* @throws Exception\TypeConverterException
* @throws Exception\InvalidTargetException
* @throws Exception\DuplicateTypeConverterException
* @throws Exception\InvalidSourceException
*
* @internal since TYPO3 v12.0
*/
protected function findTypeConverter(mixed $source, string $targetType, PropertyMappingConfigurationInterface $configuration): TypeConverterInterface
{
if ($configuration->getTypeConverter() !== null) {
return $configuration->getTypeConverter();
}
$sourceType = $this->determineSourceType($source);
$targetType = $this->parseCompositeType($targetType);
$targetType = TypeHandlingUtility::normalizeType($targetType);
return $this->typeConverterRegistry->findTypeConverter($sourceType, $targetType);
}
/**
* Determine the type of the source data, or throw an exception if source was an unsupported format.
*
* @throws Exception\InvalidSourceException
*
* @internal since TYPO3 v12.0
*/
protected function determineSourceType(mixed $source): string
{
if (is_string($source)) {
return 'string';
}
if (is_array($source)) {
return 'array';
}
if (is_float($source)) {
return 'float';
}
if (is_int($source)) {
return 'integer';
}
if (is_bool($source)) {
return 'boolean';
}
throw new Exception\InvalidSourceException('The source is not of type string, array, float, integer or boolean, but of type "' . gettype($source) . '"', 1297773150);
}
/**
* Parse a composite type like \Foo\Collection<\Bar\Entity> into
* \Foo\Collection
*
* @internal since TYPO3 v12.0
*/
protected function parseCompositeType(string $compositeType): string
{
if (str_contains($compositeType, '<')) {
$compositeType = substr($compositeType, 0, (int)strpos($compositeType, '<'));
}
return $compositeType;
}
}
@@ -0,0 +1,368 @@
<?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\Extbase\Property;
/**
* Concrete configuration object for the PropertyMapper.
*/
class PropertyMappingConfiguration implements PropertyMappingConfigurationInterface
{
/**
* Placeholder in property paths for multi-valued types
*/
public const PROPERTY_PATH_PLACEHOLDER = '*';
/**
* multi-dimensional array which stores type-converter specific configuration:
* 1. Dimension: Fully qualified class name of the type converter
* 2. Dimension: Configuration Key
* Value: Configuration Value
*
* @var array<class-string<TypeConverterInterface>, non-empty-string|int>
*/
protected array $configuration = [];
/**
* Stores the configuration for specific child properties.
*
* @var array<self::PROPERTY_PATH_PLACEHOLDER|non-empty-string,PropertyMappingConfigurationInterface>
*/
protected array $subConfigurationForProperty = [];
/**
* Keys which should be renamed
*
* @var array<non-empty-string, non-empty-string>
*/
protected array $mapping = [];
protected ?TypeConverterInterface $typeConverter = null;
/**
* List of allowed property names to be converted
*
* @var array<non-empty-string, non-empty-string>
*/
protected array $propertiesToBeMapped = [];
/**
* List of property names to be skipped during property mapping
*
* @var array<non-empty-string, non-empty-string>
*/
protected array $propertiesToSkip = [];
/**
* List of disallowed property names which will be ignored while property mapping
*
* @var array<non-empty-string, non-empty-string>
*/
protected array $propertiesNotToBeMapped = [];
/**
* If TRUE, unknown properties will be skipped during property mapping
*/
protected bool $skipUnknownProperties = false;
/**
* If TRUE, unknown properties will be mapped.
*/
protected bool $mapUnknownProperties = false;
/**
* The behavior is as follows:
*
* - if a property has been explicitly forbidden using allowAllPropertiesExcept(...), it is directly rejected
* - if a property has been allowed using allowProperties(...), it is directly allowed.
* - if allowAllProperties* has been called, we allow unknown properties
* - else, return FALSE.
*
* @param non-empty-string $propertyName
* @return bool TRUE if the given propertyName should be mapped, FALSE otherwise.
*/
public function shouldMap(string $propertyName): bool
{
if (isset($this->propertiesNotToBeMapped[$propertyName])) {
return false;
}
if (isset($this->propertiesToBeMapped[$propertyName])) {
return true;
}
if (isset($this->subConfigurationForProperty[self::PROPERTY_PATH_PLACEHOLDER])) {
return true;
}
return $this->mapUnknownProperties;
}
/**
* Check if the given $propertyName should be skipped during mapping.
*
* @param non-empty-string $propertyName
*/
public function shouldSkip(string $propertyName): bool
{
return isset($this->propertiesToSkip[$propertyName]);
}
/**
* Allow all properties in property mapping, even unknown ones.
*
* @return $this
*/
public function allowAllProperties(): self
{
$this->mapUnknownProperties = true;
return $this;
}
/**
* Allow a list of specific properties. All arguments of
* allowProperties are used here (varargs).
*
* Example: allowProperties('title', 'content', 'author')
*
* @param non-empty-string ...$propertyNames
* @return $this
*/
public function allowProperties(int|string ...$propertyNames): self
{
foreach ($propertyNames as $propertyName) {
$this->propertiesToBeMapped[$propertyName] = $propertyName;
}
return $this;
}
/**
* Skip a list of specific properties. All arguments of
* skipProperties are used here (varargs).
*
* Example: skipProperties('unused', 'dummy')
*
* @param non-empty-string ...$propertyNames
* @return $this
*/
public function skipProperties(string ...$propertyNames): self
{
foreach ($propertyNames as $propertyName) {
$this->propertiesToSkip[$propertyName] = $propertyName;
}
return $this;
}
/**
* Allow all properties during property mapping, but reject a few
* selected ones (blacklist).
*
* Example: allowAllPropertiesExcept('password', 'userGroup')
*
* @param non-empty-string ...$propertyNames
* @return $this
*/
public function allowAllPropertiesExcept(string ...$propertyNames): self
{
$this->mapUnknownProperties = true;
foreach ($propertyNames as $propertyName) {
$this->propertiesNotToBeMapped[$propertyName] = $propertyName;
}
return $this;
}
/**
* When this is enabled, properties that are disallowed will be skipped
* instead of triggering an error during mapping.
*
* @return $this
*/
public function skipUnknownProperties(): self
{
$this->skipUnknownProperties = true;
return $this;
}
/**
* Whether unknown (non configured) properties should be skipped during
* mapping, instead if causing an error.
*/
public function shouldSkipUnknownProperties(): bool
{
return $this->skipUnknownProperties;
}
/**
* Returns the sub-configuration for the passed $propertyName. Must ALWAYS return a valid configuration object!
*
* @param non-empty-string $propertyName
* @return PropertyMappingConfigurationInterface the property mapping configuration for the given $propertyName.
*/
public function getConfigurationFor(string $propertyName): PropertyMappingConfigurationInterface
{
if (isset($this->subConfigurationForProperty[$propertyName])) {
return $this->subConfigurationForProperty[$propertyName];
}
if (isset($this->subConfigurationForProperty[self::PROPERTY_PATH_PLACEHOLDER])) {
return $this->subConfigurationForProperty[self::PROPERTY_PATH_PLACEHOLDER];
}
return new self();
}
/**
* Maps the given $sourcePropertyName to a target property name.
*
* @param non-empty-string $sourcePropertyName
* @return non-empty-string property name of target
*/
public function getTargetPropertyName(string $sourcePropertyName): string
{
if (isset($this->mapping[$sourcePropertyName])) {
return $this->mapping[$sourcePropertyName];
}
return $sourcePropertyName;
}
/**
* @param class-string<TypeConverterInterface> $typeConverterClassName
* @param non-empty-string|int $key
* @return mixed configuration value for the specific $typeConverterClassName. Can be used by Type Converters to fetch converter-specific configuration.
*/
public function getConfigurationValue(string $typeConverterClassName, string|int $key): mixed
{
if (!isset($this->configuration[$typeConverterClassName][$key])) {
return null;
}
return $this->configuration[$typeConverterClassName][$key];
}
/**
* Define renaming from Source to Target property.
*
* @param non-empty-string $sourcePropertyName
* @param non-empty-string $targetPropertyName
* @return $this
*/
public function setMapping(string $sourcePropertyName, string $targetPropertyName): self
{
$this->mapping[$sourcePropertyName] = $targetPropertyName;
return $this;
}
/**
* Set all options for the given $typeConverter.
*
* @param class-string<TypeConverterInterface> $typeConverter class name of type converter
* @return $this
*/
public function setTypeConverterOptions(string $typeConverter, array $options): self
{
foreach ($this->getTypeConvertersWithParentClasses($typeConverter) as $typeConverter) {
$this->configuration[$typeConverter] = $options;
}
return $this;
}
/**
* Set a single option (denoted by $optionKey) for the given $typeConverter.
*
* @param class-string<TypeConverterInterface> $typeConverter class name of type converter
* @param non-empty-string|int $optionKey
* @param mixed $optionValue
* @return $this
*/
public function setTypeConverterOption(string $typeConverter, string|int $optionKey, mixed $optionValue): self
{
foreach ($this->getTypeConvertersWithParentClasses($typeConverter) as $typeConverter) {
$this->configuration[$typeConverter][$optionKey] = $optionValue;
}
return $this;
}
/**
* Get type converter classes including parents for the given type converter
*
* When setting an option on a subclassed type converter, this option must also be set on
* all its parent type converters.
*
* @param class-string<TypeConverterInterface> $typeConverter The type converter class
* @return array<class-string> Class names of type converters
*/
protected function getTypeConvertersWithParentClasses(string $typeConverter): array
{
$typeConverterClasses = class_parents($typeConverter);
$typeConverterClasses = $typeConverterClasses ?: [];
$typeConverterClasses[] = $typeConverter;
return $typeConverterClasses;
}
/**
* Returns the configuration for the specific property path, ready to be modified. Should be used
* inside a fluent interface like:
* $configuration->forProperty('foo.bar')->setTypeConverterOption(....)
*
* @param non-empty-string $propertyPath
*/
public function forProperty(string $propertyPath): PropertyMappingConfigurationInterface
{
$splitPropertyPath = explode('.', $propertyPath);
return $this->traverseProperties($splitPropertyPath);
}
/**
* Traverse the property configuration. Only used by forProperty().
*/
public function traverseProperties(array $splitPropertyPath): PropertyMappingConfigurationInterface
{
if (empty($splitPropertyPath)) {
return $this;
}
$currentProperty = array_shift($splitPropertyPath);
if (!isset($this->subConfigurationForProperty[$currentProperty])) {
$type = static::class;
if (isset($this->subConfigurationForProperty[self::PROPERTY_PATH_PLACEHOLDER])) {
$this->subConfigurationForProperty[$currentProperty] = clone $this->subConfigurationForProperty[self::PROPERTY_PATH_PLACEHOLDER];
} else {
$this->subConfigurationForProperty[$currentProperty] = new $type();
}
}
return $this->subConfigurationForProperty[$currentProperty]->traverseProperties($splitPropertyPath);
}
/**
* Return the type converter set for this configuration.
*/
public function getTypeConverter(): ?TypeConverterInterface
{
return $this->typeConverter;
}
/**
* Set a type converter which should be used for this specific conversion.
*
* @return $this
*/
public function setTypeConverter(TypeConverterInterface $typeConverter)
{
$this->typeConverter = $typeConverter;
return $this;
}
}
@@ -0,0 +1,46 @@
<?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\Extbase\Property;
use TYPO3\CMS\Extbase\Property\TypeConverter\PersistentObjectConverter;
/**
* This builder creates the default configuration for Property Mapping, if no configuration has been passed to the Property Mapper.
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
class PropertyMappingConfigurationBuilder
{
/**
* Builds the default property mapping configuration.
*
* @param class-string<PropertyMappingConfiguration> $type the implementation class to instantiate
* @return PropertyMappingConfiguration
*/
public function build($type = PropertyMappingConfiguration::class)
{
$configuration = new $type();
$configuration->setTypeConverterOptions(PersistentObjectConverter::class, [
PersistentObjectConverter::CONFIGURATION_CREATION_ALLOWED => true,
PersistentObjectConverter::CONFIGURATION_MODIFICATION_ALLOWED => true,
]);
$configuration->allowAllProperties();
return $configuration;
}
}
@@ -0,0 +1,100 @@
<?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\Extbase\Property;
/**
* Configuration object for the property mapper.
*/
interface PropertyMappingConfigurationInterface
{
/**
* returns TRUE if the given propertyName should be mapped, FALSE otherwise.
*
* @param non-empty-string $propertyName
*/
public function shouldSkip(string $propertyName): bool;
/**
* Whether unknown (unconfigured) properties should be skipped during
* mapping, instead if causing an error.
*/
public function shouldSkipUnknownProperties(): bool;
/**
* Returns the sub-configuration for the passed $propertyName. Must ALWAYS return a valid configuration object!
*
* @param non-empty-string $propertyName
* @return PropertyMappingConfigurationInterface the property mapping configuration for the given $propertyName.
*/
public function getConfigurationFor(string $propertyName): PropertyMappingConfigurationInterface;
/**
* Maps the given $sourcePropertyName to a target property name.
* Can be used to rename properties from source to target.
*
* @param non-empty-string $sourcePropertyName
* @return non-empty-string property name of target
*/
public function getTargetPropertyName(string $sourcePropertyName): string;
/**
* @param class-string<TypeConverterInterface> $typeConverterClassName
* @param non-empty-string|int $key
* @return mixed configuration value for the specific $typeConverterClassName. Can be used by Type Converters to fetch converter-specific configuration
*/
public function getConfigurationValue(string $typeConverterClassName, string|int $key): mixed;
/**
* This method can be used to explicitly force a TypeConverter to be used for this Configuration.
*
* @return TypeConverterInterface|null The type converter to be used for this particular PropertyMappingConfiguration, or NULL if the system-wide configured type converter should be used.
*/
public function getTypeConverter(): ?TypeConverterInterface;
public function allowAllProperties(): PropertyMappingConfigurationInterface;
/**
* @param class-string<TypeConverterInterface> $typeConverter
*/
public function setTypeConverterOption(string $typeConverter, string|int $optionKey, mixed $optionValue): PropertyMappingConfigurationInterface;
/**
* @param non-empty-string $propertyName
*/
public function shouldMap(string $propertyName): bool;
/**
* @param class-string<TypeConverterInterface> $typeConverter
*/
public function setTypeConverterOptions(string $typeConverter, array $options): PropertyMappingConfigurationInterface;
/**
* @param non-empty-string $propertyPath
*/
public function forProperty(string $propertyPath): PropertyMappingConfigurationInterface;
/**
* @param non-empty-string ...$propertyNames
*/
public function allowProperties(string ...$propertyNames): PropertyMappingConfigurationInterface;
/**
* @param array $splitPropertyPath
*/
public function traverseProperties(array $splitPropertyPath): PropertyMappingConfigurationInterface;
}
@@ -0,0 +1,72 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\ResourceInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Domain\Model\File;
use TYPO3\CMS\Extbase\Domain\Model\FileReference;
use TYPO3\CMS\Extbase\Domain\Model\Folder;
use TYPO3\CMS\Extbase\Property\Exception;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
/**
* Converter which transforms simple types to \TYPO3\CMS\Extbase\Domain\Model\File.
*
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
abstract class AbstractFileFolderConverter extends AbstractTypeConverter
{
protected string $expectedObjectType;
protected ResourceFactory $fileFactory;
public function injectFileFactory(ResourceFactory $fileFactory): void
{
$this->fileFactory = $fileFactory;
}
/**
* Actually convert from $source to $targetType, taking into account the fully
* built $convertedChildProperties and $configuration.
*
* @param string|int $source
* @throws Exception
*/
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): File|FileReference|Folder {
$object = $this->getOriginalResource($source);
if (empty($this->expectedObjectType) || !$object instanceof $this->expectedObjectType) {
throw new Exception('Expected object of type "' . $this->expectedObjectType . '" but got ' . (is_object($object) ? get_class($object) : 'null'), 1342895975);
}
/** @var File|FileReference|Folder $subject */
$subject = GeneralUtility::makeInstance($targetType);
$subject->setOriginalResource($object);
return $subject;
}
/**
* @param string|int $source
*/
abstract protected function getOriginalResource($source): ?ResourceInterface;
}
@@ -0,0 +1,71 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
use TYPO3\CMS\Extbase\Property\TypeConverterInterface;
/**
* Type converter which provides sensible default implementations for most methods. If you extend this class
* you only need to implement convertFrom()
*/
abstract class AbstractTypeConverter implements TypeConverterInterface, SingletonInterface
{
/**
* @todo The concept of this method is flawed because it enables the override of the target type depending on the
* structure of the source. So, technically we no longer convert type A to B but source of type A with
* structure X to type B defined by X. This makes a type converter non-deterministic.
*
* Returns the $originalTargetType unchanged in this implementation.
*
* @param mixed $source the source data
* @param string $originalTargetType the type we originally want to convert to
*/
public function getTargetTypeForSource($source, string $originalTargetType, ?PropertyMappingConfigurationInterface $configuration = null): string
{
return $originalTargetType;
}
/**
* @todo this method is only used for converter sources that have children (i.e. objects). Introduce another
* ChildPropertyAwareTypeConverterInterface and drop this method from the main interface
*
* Returns an empty list of sub property names
*
* @param mixed $source
*/
public function getSourceChildPropertiesToBeConverted($source): array
{
return [];
}
/**
* @todo this method is only used for converter sources that have children (i.e. objects). Introduce another
* ChildPropertyAwareTypeConverterInterface and drop this method from the main interface
*
* This method is never called, as getSourceChildPropertiesToBeConverted() returns an empty array.
*
* @param string $targetType
* @param string $propertyName
*/
public function getTypeOfChildProperty(string $targetType, string $propertyName, PropertyMappingConfigurationInterface $configuration): string
{
return '';
}
}
@@ -0,0 +1,65 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Property\Exception\TypeConverterException;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
/**
* Converter which transforms strings/arrays to arrays.
*/
class ArrayConverter extends AbstractTypeConverter
{
public const CONFIGURATION_DELIMITER = 'delimiter';
public const CONFIGURATION_REMOVE_EMPTY_VALUES = 'removeEmptyValues';
public const CONFIGURATION_LIMIT = 'limit';
/**
* Convert from $source to $targetType, a noop if the source is an array.
* If it is an empty string it will be converted to an empty array.
* If the type converter has a configuration, it can convert non-empty strings, too
*
* @param string|array $source
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): array|string {
if (!is_string($source)) {
return $source;
}
if ($source === '') {
return [];
}
if ($configuration === null) {
return $source;
}
$delimiter = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_DELIMITER);
$removeEmptyValues = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_REMOVE_EMPTY_VALUES) ?? false;
$limit = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_LIMIT) ?? 0;
if (!is_string($delimiter)) {
throw new TypeConverterException('No delimiter configured for ' . self::class . ' and non-empty value given.', 1582877555);
}
return GeneralUtility::trimExplode($delimiter, $source, $removeEmptyValues, $limit);
}
}
@@ -0,0 +1,41 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
/**
* Converter which transforms simple types to a boolean, by simply casting it.
*/
class BooleanConverter extends AbstractTypeConverter
{
/**
* Actually convert from $source to $targetType
*
* @param mixed $source
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): bool {
return (bool)$source;
}
}
@@ -0,0 +1,48 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Core\Type\Exception\InvalidValueExceptionInterface;
use TYPO3\CMS\Extbase\Error\Error;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
/**
* Converter which transforms simple types to a core type
* implementing \TYPO3\CMS\Core\Type\TypeInterface.
*/
class CoreTypeConverter extends AbstractTypeConverter
{
/**
* Convert an object from $source to an Enumeration.
*
* @param mixed $source
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): object {
try {
return new $targetType($source);
} catch (InvalidValueExceptionInterface $exception) {
return new Error($exception->getMessage(), 1381680012);
}
}
}
@@ -0,0 +1,61 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Core\Country\Country;
use TYPO3\CMS\Core\Country\CountryProvider;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
/**
* Converter which transforms simple types to a country object.
*/
class CountryConverter extends AbstractTypeConverter
{
public const CONFIGURATION_FROM = 'alpha2IsoCode';
protected CountryProvider $countryProvider;
public function injectCountryProvider(CountryProvider $countryProvider): void
{
$this->countryProvider = $countryProvider;
}
/**
* Actually convert from $source to $targetType, taking into account the fully
* built $convertedChildProperties and $configuration.
*
* @param string $source
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): ?Country {
$by = self::CONFIGURATION_FROM;
if ($configuration !== null) {
$by = $configuration->getConfigurationValue(CountryConverter::class, self::CONFIGURATION_FROM);
}
return match ($by) {
'alpha3IsoCode' => $this->countryProvider->getByAlpha3IsoCode((string)$source),
default => $this->countryProvider->getByAlpha2IsoCode((string)$source),
};
}
}
@@ -0,0 +1,200 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Extbase\Error\Error;
use TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException;
use TYPO3\CMS\Extbase\Property\Exception\TypeConverterException;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
/**
* Converter which transforms from different input formats into DateTime objects.
*
* Source can be either a string or an array. The date string is expected to be formatted
* according to DEFAULT_DATE_FORMAT.
*
* But the default date format can be overridden in the initialize*Action() method like this::
*
* $this->arguments['<argumentName>']
* ->getPropertyMappingConfiguration()
* ->forProperty('<propertyName>') // this line can be skipped in order to specify the format for all properties
* ->setTypeConverterOption(\TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter::class, \TYPO3\CMS\Extbase\Property\TypeConverter\DateTimeConverter::CONFIGURATION_DATE_FORMAT, '<dateFormat>');
*
* If the source is of type array, it is possible to override the format in the source::
*
* array(
* 'date' => '<dateString>',
* 'dateFormat' => '<dateFormat>'
* );
*
* By using an array as source you can also override time and timezone of the created DateTime object::
*
* array(
* 'date' => '<dateString>',
* 'hour' => '<hour>', // integer
* 'minute' => '<minute>', // integer
* 'seconds' => '<seconds>', // integer
* 'timezone' => '<timezone>', // string, see http://www.php.net/manual/timezones.php
* );
*
* As an alternative to providing the date as string, you might supply day, month and year as array items each::
*
* array(
* 'day' => '<day>', // integer
* 'month' => '<month>', // integer
* 'year' => '<year>', // integer
* );
*/
class DateTimeConverter extends AbstractTypeConverter
{
/**
* @var string
*/
public const CONFIGURATION_DATE_FORMAT = 'dateFormat';
/**
* The default date format is "YYYY-MM-DDT##:##:##+##:##", for example "2005-08-15T15:52:01+00:00"
* according to the W3C standard @see http://www.w3.org/TR/NOTE-datetime.html
*
* @var string
*/
public const DEFAULT_DATE_FORMAT = \DateTimeInterface::W3C;
/**
* Converts $source to a \DateTime using the configured dateFormat
*
* @param string|int|array $source the string to be converted to a \DateTime object
* @param string $targetType must be "DateTime"
* @param array $convertedChildProperties not used currently
* @throws TypeConverterException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): \DateTime|Error|null {
$dateFormat = $this->getDefaultDateFormat($configuration);
if (is_string($source)) {
$dateAsString = $source;
} elseif (is_int($source)) {
$dateAsString = (string)$source;
} else {
if (isset($source['date']) && is_string($source['date'])) {
$dateAsString = $source['date'];
} elseif (isset($source['date']) && is_int($source['date'])) {
$dateAsString = (string)$source['date'];
} elseif ($this->isDatePartKeysProvided($source)) {
if ($source['day'] < 1 || $source['month'] < 1 || $source['year'] < 1) {
return new Error('Could not convert the given date parts into a DateTime object because one or more parts were 0.', 1333032779);
}
$dateAsString = sprintf('%d-%d-%d', $source['year'], $source['month'], $source['day']);
} else {
throw new TypeConverterException('Could not convert the given source into a DateTime object because it was not an array with a valid date as a string', 1308003914);
}
if (isset($source['dateFormat']) && $source['dateFormat'] !== '') {
$dateFormat = $source['dateFormat'];
}
}
if ($dateAsString === '') {
return null;
}
if (ctype_digit($dateAsString) && $configuration === null && (!is_array($source) || !isset($source['dateFormat']))) {
// todo: type converters are never called without a property mapping configuration
$dateFormat = 'U';
}
if (is_array($source) && isset($source['timezone']) && (string)$source['timezone'] !== '') {
try {
$timezone = new \DateTimeZone($source['timezone']);
} catch (\Exception $e) {
throw new TypeConverterException('The specified timezone "' . $source['timezone'] . '" is invalid.', 1308240974);
}
$date = $targetType::createFromFormat($dateFormat, $dateAsString, $timezone);
} else {
$date = $targetType::createFromFormat($dateFormat, $dateAsString);
}
if ($date === false) {
return new \TYPO3\CMS\Extbase\Validation\Error(
$this->translateErrorMessage(
'LLL:EXT:extbase/Resources/Private/Language/locallang.xlf:converter.datetime.notrecognized',
),
1307719788,
[$dateAsString, $dateFormat]
);
}
if (is_array($source)) {
$date = $this->overrideTimeIfSpecified($date, $source);
}
return $date;
}
/**
* Wrap static call to LocalizationUtility to simplify unit testing.
*/
protected function translateErrorMessage(string $translateKey): string
{
return LocalizationUtility::translate($translateKey) ?? '';
}
/**
* Returns whether date information (day, month, year) are present as keys in $source.
*/
protected function isDatePartKeysProvided(array $source): bool
{
return isset($source['day'], $source['month'], $source['year'])
&& ctype_digit($source['day']) && ctype_digit($source['month']) && ctype_digit($source['year']);
}
/**
* Determines the default date format to use for the conversion.
* If no format is specified in the mapping configuration DEFAULT_DATE_FORMAT is used.
*
* @throws InvalidPropertyMappingConfigurationException
*/
protected function getDefaultDateFormat(?PropertyMappingConfigurationInterface $configuration = null): string
{
if ($configuration === null) {
// todo: type converters are never called without a property mapping configuration
return self::DEFAULT_DATE_FORMAT;
}
$dateFormat = $configuration->getConfigurationValue(DateTimeConverter::class, self::CONFIGURATION_DATE_FORMAT);
if ($dateFormat === null) {
return self::DEFAULT_DATE_FORMAT;
}
if (!is_string($dateFormat)) {
throw new InvalidPropertyMappingConfigurationException('CONFIGURATION_DATE_FORMAT must be of type string, "' . get_debug_type($dateFormat) . '" given', 1307719569);
}
return $dateFormat;
}
/**
* Overrides hour, minute & second of the given date with the values in the $source array
*/
protected function overrideTimeIfSpecified(\DateTime $date, array $source): \DateTime
{
if (!isset($source['hour']) && !isset($source['minute']) && !isset($source['second'])) {
return $date;
}
$hour = isset($source['hour']) ? (int)$source['hour'] : 0;
$minute = isset($source['minute']) ? (int)$source['minute'] : 0;
$second = isset($source['second']) ? (int)$source['second'] : 0;
return $date->setTime($hour, $minute, $second);
}
}
@@ -0,0 +1,71 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
use UnitEnum;
/**
* Converter which transforms strings/integers/floats to Enum Instance.
*/
class EnumConverter extends AbstractTypeConverter
{
/**
* Convert an enum from $source to an enum.
*
* @template T of UnitEnum
* @param class-string<T> $targetType
* @return T|null
* @throws InvalidTargetException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom(
mixed $source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): ?\UnitEnum {
return $this->getEnumElement($source, $targetType);
}
/**
* @template T of UnitEnum
* @param class-string<T> $targetType
* @return T|null
* @throws InvalidTargetException
*/
protected function getEnumElement(float|int|string $source, string $targetType): ?\UnitEnum
{
if (!enum_exists($targetType)) {
throw new InvalidTargetException('TargetType "' . $targetType . '" is not an enum.', 1660834545);
}
foreach ($targetType::cases() as $enum) {
if (property_exists($enum, 'value') && $enum->value == $source) {
return $enum;
}
}
foreach ($targetType::cases() as $enum) {
if ($enum->name == $source) {
return $enum;
}
}
return null;
}
}
@@ -0,0 +1,41 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceInterface;
/**
* Converter which transforms simple types to \TYPO3\CMS\Extbase\Domain\Model\File.
*
* @internal
*/
class FileConverter extends AbstractFileFolderConverter
{
protected string $expectedObjectType = File::class;
/**
* @param string|int $source
* @throws ResourceDoesNotExistException
*/
protected function getOriginalResource($source): ?ResourceInterface
{
return $this->fileFactory->retrieveFileOrFolderObject($source);
}
}
@@ -0,0 +1,39 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Core\Resource\ResourceInterface;
/**
* Converter which transforms simple types to \TYPO3\CMS\Extbase\Domain\Model\FileReference.
*
* @internal
*/
class FileReferenceConverter extends AbstractFileFolderConverter
{
protected string $expectedObjectType = FileReference::class;
/**
* @param string|int $source
*/
protected function getOriginalResource($source): ?ResourceInterface
{
return $this->fileFactory->getFileReferenceObject($source);
}
}
@@ -0,0 +1,65 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Extbase\Error\Error;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
/**
* Converter which transforms a simple type to a float.
*
* This is basically done by simply casting it.
*/
class FloatConverter extends AbstractTypeConverter
{
/**
* @var string
*/
public const CONFIGURATION_THOUSANDS_SEPARATOR = 'thousandsSeparator';
/**
* @var string
*/
public const CONFIGURATION_DECIMAL_POINT = 'decimalPoint';
/**
* Actually convert from $source to $targetType, by doing a typecast.
*
* @param mixed $source
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): float|Error|null {
if ($source === null || (string)$source === '') {
return null;
}
if (is_string($source) && $configuration !== null) {
$thousandsSeparator = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_THOUSANDS_SEPARATOR);
$decimalPoint = $configuration->getConfigurationValue(self::class, self::CONFIGURATION_DECIMAL_POINT);
$source = str_replace([$thousandsSeparator, $decimalPoint], ['', '.'], $source);
}
if (!is_numeric($source)) {
return new Error('"%s" cannot be converted to a float value.', 1332934124, [$source]);
}
return (float)$source;
}
}
@@ -0,0 +1,39 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceInterface;
/**
* Converter which transforms simple types to \TYPO3\CMS\Extbase\Domain\Model\Folder.
*
* @internal
*/
class FolderConverter extends AbstractFileFolderConverter
{
protected string $expectedObjectType = Folder::class;
/**
* @param string $source
*/
protected function getOriginalResource($source): ?ResourceInterface
{
return $this->fileFactory->getFolderObjectFromCombinedIdentifier($source);
}
}
@@ -0,0 +1,48 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Extbase\Error\Error;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
/**
* Converter which transforms a simple type to an integer, by simply casting it.
*/
class IntegerConverter extends AbstractTypeConverter
{
/**
* Actually convert from $source to $targetType, in fact a noop here.
*
* @param int|string|null $source
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): int|Error|null {
if ($source === null || (string)$source === '') {
return null;
}
if (!is_numeric($source)) {
return new Error('"%s" is no integer.', 1332933658, [$source]);
}
return (int)$source;
}
}
@@ -0,0 +1,264 @@
<?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\Extbase\Property\TypeConverter;
use Psr\Container\ContainerInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Property\Exception\InvalidDataTypeException;
use TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException;
use TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodException;
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodParameterException;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
/**
* This converter transforms arrays to simple objects (POPO) by setting properties.
*/
class ObjectConverter extends AbstractTypeConverter
{
/**
* @var int
*/
public const CONFIGURATION_TARGET_TYPE = 3;
/**
* @var int
*/
public const CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED = 4;
protected ContainerInterface $container;
protected ReflectionService $reflectionService;
public function injectReflectionService(ReflectionService $reflectionService): void
{
$this->reflectionService = $reflectionService;
}
public function injectContainer(ContainerInterface $container): void
{
$this->container = $container;
}
/**
* Convert all properties in the source array
*
* @param mixed $source
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getSourceChildPropertiesToBeConverted($source): array
{
if (isset($source['__type'])) {
unset($source['__type']);
}
return $source;
}
/**
* The type of a property is determined by the reflection service.
*
* @throws InvalidTargetException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getTypeOfChildProperty(
string $targetType,
string $propertyName,
PropertyMappingConfigurationInterface $configuration
): string {
$configuredTargetType = $configuration->getConfigurationFor($propertyName)
->getConfigurationValue(ObjectConverter::class, self::CONFIGURATION_TARGET_TYPE);
if ($configuredTargetType !== null) {
return $configuredTargetType;
}
$classSchema = $this->reflectionService->getClassSchema($targetType);
// @todo: infer property type from property instead of from setter and make setter optional
// {@link https://forge.typo3.org/issues/100136}
$methodName = 'set' . ucfirst($propertyName);
if ($classSchema->hasMethod($methodName)) {
$methodParameters = $classSchema->getMethod($methodName)->getParameters();
$methodParameter = current($methodParameters);
if ($methodParameter->getType() === null) {
throw new InvalidTargetException('Setter for property "' . $propertyName . '" had no type hint or documentation in target object of type "' . $targetType . '".', 1303379158);
}
$property = $classSchema->getProperty($propertyName);
$primaryCollectionValueType = $property->getPrimaryCollectionValueType();
if ($primaryCollectionValueType) {
return $methodParameter->getType() . '<' . ($primaryCollectionValueType->getClassName() ?? $primaryCollectionValueType->getBuiltinType()) . '>';
}
return $methodParameter->getType();
}
try {
$parameterType = $classSchema->getMethod('__construct')->getParameter($propertyName)->getType();
} catch (NoSuchMethodException $e) {
$exceptionMessage = sprintf('Type of child property "%s" of class "%s" could not be '
. 'derived from constructor arguments as said class does not have a constructor '
. 'defined.', $propertyName, $targetType);
throw new InvalidTargetException($exceptionMessage, 1582385098);
} catch (NoSuchMethodParameterException $e) {
$exceptionMessage = sprintf('Type of child property "%1$s" of class "%2$s" could not be '
. 'derived from constructor arguments as the constructor of said class does not '
. 'have a parameter with property name "%1$s".', $propertyName, $targetType);
throw new InvalidTargetException($exceptionMessage, 1303379126);
}
if ($parameterType === null) {
$exceptionMessage = sprintf('Type of child property "%1$s" of class "%2$s" could not be '
. 'derived from constructor argument "%1$s". This usually happens if the argument '
. 'misses a type hint.', $propertyName, $targetType);
throw new InvalidTargetException($exceptionMessage, 1582385619);
}
return $parameterType;
}
/**
* Convert an object from $source to an object.
*
* @param mixed $source
* @return object|null the target type
* @throws InvalidTargetException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): ?object {
$object = $this->buildObject($convertedChildProperties, $targetType);
foreach ($convertedChildProperties as $propertyName => $propertyValue) {
$result = ObjectAccess::setProperty($object, $propertyName, $propertyValue);
if ($result === false) {
$exceptionMessage = sprintf(
'Property "%s" having a value of type "%s" could not be set in target object of type "%s". Make sure that the property is accessible properly, for example via an appropriate setter method.',
$propertyName,
get_debug_type($propertyValue),
$targetType
);
throw new InvalidTargetException($exceptionMessage, 1304538165);
}
}
return $object;
}
/**
* Determines the target type based on the source's (optional) __type key and by evaluating possible
* XCLASS overrides of the target type.
*
* @param mixed $source
* @throws InvalidDataTypeException
* @throws InvalidPropertyMappingConfigurationException
* @throws \InvalidArgumentException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getTargetTypeForSource(
$source,
string $originalTargetType,
?PropertyMappingConfigurationInterface $configuration = null
): string {
$targetType = $originalTargetType;
if (is_array($source) && array_key_exists('__type', $source)) {
$targetType = $source['__type'];
if ($configuration === null) {
// todo: this is impossible to achieve since this methods is always called via (convert -> doMapping -> getTargetTypeForSource) and convert and doMapping create configuration objects if missing.
throw new \InvalidArgumentException('A property mapping configuration must be given, not NULL.', 1326277369);
}
if ($configuration->getConfigurationValue(ObjectConverter::class, self::CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED) !== true) {
throw new InvalidPropertyMappingConfigurationException('Override of target type not allowed. To enable this, you need to set the PropertyMappingConfiguration Value "CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED" to TRUE.', 1317050430);
}
if ($targetType !== $originalTargetType && is_a($targetType, $originalTargetType, true) === false) {
throw new InvalidDataTypeException('The given type "' . $targetType . '" is not a subtype of "' . $originalTargetType . '".', 1317048056);
}
}
// Respect XCLASSed object target type
return GeneralUtility::getClassName($targetType);
}
/**
* Builds a new instance of $objectType with the given $possibleConstructorArgumentValues. If
* constructor argument values are missing from the given array the method looks for a default
* value in the constructor signature. Furthermore, the constructor arguments are removed from
* $possibleConstructorArgumentValues: They are considered "handled" by __construct and will
* not be mapped calling setters later.
*
* @return object The created instance
* @throws InvalidTargetException if a required constructor argument is missing
*/
protected function buildObject(array &$possibleConstructorArgumentValues, string $objectType): object
{
// The ObjectConverter typically kicks in, if request arguments are to be mapped to
// a domain model. An example is ext:belog:Domain/Model/Demand.
// Domain models are data objects and should thus be fetched via makeInstance(), should
// not be registered as service, and should thus not be DI aware.
// Additionally, all to-be-mapped arguments are hand over as "possible constructor arguments" here,
// and extbase is able to use single arguments as constructor arguments to domain models,
// if a __construct() with an argument having the same name as a to-be-mapped argument exists.
// This is the reason that &$possibleConstructorArgumentValues is hand over as reference here:
// If an argument can be hand over as constructor argument, it is considered "already mapped" and
// is not manually mapped calling setters later.
// To be as backwards compatible as possible, the following logic is applied:
// * If the class is registered as service (container->has()=true), and if there are no
// $possibleConstructorArgumentValues, instantiate the class via container->get(). Easy
// scenario - the target class is DI aware and will get dependencies injected. A different target
// class can be specified using service configuration if needed.
// * If the class is registered as service, and if there are $possibleConstructorArgumentValues,
// the class is instantiated via container->get(). $possibleConstructorArgumentValues are *not* hand
// over to the constructor. The target class can then use constructor injection and inject* methods
// for DI. A different target class can be specified using service configuration if needed. Mapping
// of arguments is done using setters by follow-up code.
// * If the class is *not* registered as service, makeInstance() is used for object retrieval.
// * If there are no $possibleConstructorArgumentValues, makeInstance() is used right away.
// * If there are $possibleConstructorArgumentValues and __construct() does not exist, makeInstance()
// is used without constructor arguments. Mapping of argument values via setters is done by follow-up code.
// * If there are $possibleConstructorArgumentValues and if __construct() exists, extbase reflection
// is used to map single arguments to constructor arguments with the same name and
// makeInstance() is used to instantiate the class. Mapping remaining arguments is done by follow-up code.
if ($this->container->has($objectType)) {
// @todo: consider dropping container->get() to prevent domain models being treated as services in >=v12.
return $this->container->get($objectType);
}
if (empty($possibleConstructorArgumentValues) || !method_exists($objectType, '__construct')) {
return GeneralUtility::makeInstance($objectType);
}
$classSchema = $this->reflectionService->getClassSchema($objectType);
$constructor = $classSchema->getMethod('__construct');
$constructorArguments = [];
foreach ($constructor->getParameters() as $parameterName => $parameter) {
if (array_key_exists($parameterName, $possibleConstructorArgumentValues)) {
$constructorArguments[] = $possibleConstructorArgumentValues[$parameterName];
unset($possibleConstructorArgumentValues[$parameterName]);
} elseif ($parameter->isOptional()) {
$constructorArguments[] = $parameter->getDefaultValue();
} else {
throw new InvalidTargetException('Missing constructor argument "' . $parameterName . '" for object of type "' . $objectType . '".', 1268734872);
}
}
return GeneralUtility::makeInstance(...[$objectType, ...$constructorArguments]);
}
}
@@ -0,0 +1,75 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
/**
* Converter which transforms simple types to an ObjectStorage.
*/
class ObjectStorageConverter extends AbstractTypeConverter
{
/**
* Actually convert from $source to $targetType, taking into account the fully
* built $convertedChildProperties and $configuration.
*
* @param mixed $source
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): ObjectStorage {
$objectStorage = new ObjectStorage();
foreach ($convertedChildProperties as $subProperty) {
$objectStorage->attach($subProperty);
}
return $objectStorage;
}
/**
* Returns the source, if it is an array, otherwise an empty array.
*
* @param mixed $source
*/
public function getSourceChildPropertiesToBeConverted($source): array
{
if (is_array($source)) {
return $source;
}
return [];
}
/**
* Return the type of a given sub-property inside the $targetType
*
* @param string $targetType
*/
public function getTypeOfChildProperty(
$targetType,
string $propertyName,
PropertyMappingConfigurationInterface $configuration
): string {
$parsedTargetType = TypeHandlingUtility::parseType($targetType);
return $parsedTargetType['elementType'];
}
}
@@ -0,0 +1,226 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject;
use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
use TYPO3\CMS\Extbase\Property\Exception\InvalidPropertyMappingConfigurationException;
use TYPO3\CMS\Extbase\Property\Exception\InvalidSourceException;
use TYPO3\CMS\Extbase\Property\Exception\InvalidTargetException;
use TYPO3\CMS\Extbase\Property\Exception\TargetNotFoundException;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoPropertyTypesException;
use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
/**
* This converter transforms arrays or strings to persistent objects. It does the following:
*
* - If the input is string, it is assumed to be a UID. Then, the object is fetched from persistence.
* - If the input is array, we check if it has an identity property.
*
* - If the input has an identity property and NO additional properties, we fetch the object from persistence.
* - If the input has an identity property AND additional properties, we fetch the object from persistence,
* and set the sub-properties. We only do this if the configuration option "CONFIGURATION_MODIFICATION_ALLOWED" is TRUE.
* - If the input has NO identity property, but additional properties, we create a new object and return it.
* However, we only do this if the configuration option "CONFIGURATION_CREATION_ALLOWED" is TRUE.
*/
class PersistentObjectConverter extends ObjectConverter
{
/**
* @var int
*/
public const CONFIGURATION_MODIFICATION_ALLOWED = 1;
/**
* @var int
*/
public const CONFIGURATION_CREATION_ALLOWED = 2;
protected PersistenceManagerInterface $persistenceManager;
/**
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function injectPersistenceManager(PersistenceManagerInterface $persistenceManager): void
{
$this->persistenceManager = $persistenceManager;
}
/**
* All properties in the source array except __identity are sub-properties.
*
* @param mixed $source
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getSourceChildPropertiesToBeConverted($source): array
{
if (is_string($source) || is_int($source)) {
return [];
}
if (isset($source['__identity'])) {
unset($source['__identity']);
}
return parent::getSourceChildPropertiesToBeConverted($source);
}
/**
* The type of a property is determined by the reflection service.
*
* @param string $targetType
* @throws InvalidTargetException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function getTypeOfChildProperty(
$targetType,
string $propertyName,
PropertyMappingConfigurationInterface $configuration
): string {
$configuredTargetType = $configuration->getConfigurationFor($propertyName)
->getConfigurationValue(PersistentObjectConverter::class, self::CONFIGURATION_TARGET_TYPE);
if ($configuredTargetType !== null) {
return $configuredTargetType;
}
$schema = $this->reflectionService->getClassSchema($targetType);
if (!$schema->hasProperty($propertyName)) {
throw new InvalidTargetException('Property "' . $propertyName . '" was not found in target object of type "' . $targetType . '".', 1297978366);
}
$primaryType = $schema->getProperty($propertyName)->getPrimaryType();
if (!$primaryType) {
throw NoPropertyTypesException::create($targetType, $propertyName);
}
$type = $primaryType->getClassName() ?? $primaryType->getBuiltinType();
if ($primaryType->isCollection() && $primaryType->getCollectionValueTypes() !== []) {
$primaryCollectionValueType = $primaryType->getCollectionValueTypes()[0];
$collectionValueType = $primaryCollectionValueType->getClassName() ?? $primaryCollectionValueType->getBuiltinType();
$type .= '<' . $collectionValueType . '>';
}
return $type;
}
/**
* Convert an object from $source to an entity or a value object.
*
* @param mixed $source
* @throws \InvalidArgumentException
* @throws InvalidTargetException
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): ?object {
if (is_array($source)) {
if (
class_exists($targetType)
&& is_subclass_of($targetType, AbstractValueObject::class)
) {
// Unset identity for valueobject to use constructor mapping, since the identity is determined from
// constructor arguments
unset($source['__identity']);
}
$object = $this->handleArrayData($source, $targetType, $convertedChildProperties, $configuration);
} elseif (is_string($source) || is_int($source)) {
if (empty($source)) {
return null;
}
$object = $this->fetchObjectFromPersistence($source, $targetType);
} else {
// todo: this case is impossible as this converter is never called with a source that is not an integer, a string or an array
throw new \InvalidArgumentException('Only integers, strings and arrays are accepted.', 1305630314);
}
foreach ($convertedChildProperties as $propertyName => $propertyValue) {
$result = ObjectAccess::setProperty($object, $propertyName, $propertyValue);
if ($result === false) {
$exceptionMessage = sprintf(
'Property "%s" having a value of type "%s" could not be set in target object of type "%s". Make sure that the property is accessible properly, for example via an appropriate setter method.',
$propertyName,
get_debug_type($propertyValue),
$targetType
);
throw new InvalidTargetException($exceptionMessage, 1297935345);
}
}
return $object;
}
/**
* Handle the case if $source is an array.
*
* @throws InvalidPropertyMappingConfigurationException
*/
protected function handleArrayData(
array $source,
string $targetType,
array &$convertedChildProperties,
?PropertyMappingConfigurationInterface $configuration = null
): object {
if (isset($source['__identity'])) {
$object = $this->fetchObjectFromPersistence($source['__identity'], $targetType);
if (count($source) > 1 && ($configuration === null || $configuration->getConfigurationValue(PersistentObjectConverter::class, self::CONFIGURATION_MODIFICATION_ALLOWED) !== true)) {
throw new InvalidPropertyMappingConfigurationException('Modification of persistent objects not allowed. To enable this, you need to set the PropertyMappingConfiguration Value "CONFIGURATION_MODIFICATION_ALLOWED" to TRUE.', 1297932028);
}
} else {
if ($configuration === null || $configuration->getConfigurationValue(PersistentObjectConverter::class, self::CONFIGURATION_CREATION_ALLOWED) !== true) {
throw new InvalidPropertyMappingConfigurationException(
'Creation of objects not allowed. To enable this, you need to set the PropertyMappingConfiguration Value "CONFIGURATION_CREATION_ALLOWED" to TRUE',
1476044961
);
}
$object = $this->buildObject($convertedChildProperties, $targetType);
}
return $object;
}
/**
* Fetch an object from persistence layer.
*
* @throws TargetNotFoundException
* @throws InvalidSourceException
*/
protected function fetchObjectFromPersistence(mixed $identity, string $targetType): object
{
// @todo - Ideally, this underscore notation should not be passed here.
// Consumers of this method should rather earlier resolve to the proper uid
// via '$value->getUid' like 'renderHiddenIdentityField' does, for example.
// @see #105319
if (str_contains((string)$identity, '_')) {
$localizedUidParts = explode('_', (string)$identity);
$uidIdentity = $localizedUidParts[0];
} else {
$uidIdentity = (string)$identity;
}
if (ctype_digit($uidIdentity)) {
$object = $this->persistenceManager->getObjectByIdentifier($uidIdentity, $targetType);
} else {
throw new InvalidSourceException('The identity property "' . $identity . '" is no UID.', 1297931020);
}
if ($object === null) {
throw new TargetNotFoundException(sprintf('Object of type %s with identity "%s" not found.', $targetType, print_r($identity, true)), 1297933823);
}
return $object;
}
}
@@ -0,0 +1,42 @@
<?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\Extbase\Property\TypeConverter;
use TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface;
/**
* Converter which transforms simple types to a string.
*/
class StringConverter extends AbstractTypeConverter
{
/**
* Actually convert from $source to $targetType, taking into account the fully
* built $convertedChildProperties and $configuration.
*
* @param string $source
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
public function convertFrom(
$source,
string $targetType,
array $convertedChildProperties = [],
?PropertyMappingConfigurationInterface $configuration = null
): string {
return (string)$source;
}
}
@@ -0,0 +1,69 @@
<?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\Extbase\Property;
/**
* Interface for type converters, which can convert from a simple type to an object or another simple type.
*
* All Type Converters should have NO INTERNAL STATE, such that they can be used as singletons and multiple times in succession (as this improves performance dramatically).
*/
interface TypeConverterInterface
{
/**
* Returns the type for a given source, depending on e.g. the __type setting or other properties.
*
* @param mixed $source the source data
* @param string $originalTargetType the type we originally want to convert to
*/
public function getTargetTypeForSource($source, string $originalTargetType, ?PropertyMappingConfigurationInterface $configuration = null): string;
/**
* Return a list of sub-properties inside the source object.
* The "key" is the sub-property name, and the "value" is the value of the sub-property.
*
* @param mixed $source
*/
public function getSourceChildPropertiesToBeConverted($source): array;
/**
* Return the type of a given sub-property inside the $targetType
*
* @return string the type of $propertyName in $targetType
*/
public function getTypeOfChildProperty(string $targetType, string $propertyName, PropertyMappingConfigurationInterface $configuration): string;
/**
* Actually convert from $source to $targetType, taking into account the fully
* built $convertedChildProperties and $configuration.
*
* The return value can be one of three types:
* - an arbitrary object, or a simple type (which has been created while mapping).
* This is the normal case.
* - NULL, indicating that this object should *not* be mapped (i.e. a "File Upload" Converter could return NULL if no file has been uploaded, and a silent failure should occur.
* - An instance of \TYPO3\CMS\Extbase\Error\Error -- This will be a user-visible error message later on.
* Furthermore, it should throw an Exception if an unexpected failure (like a security error) occurred or a configuration issue happened.
*
* @param mixed $source
* @param string $targetType
* @param array $convertedChildProperties
* @param \TYPO3\CMS\Extbase\Property\PropertyMappingConfigurationInterface $configuration
* @return mixed|\TYPO3\CMS\Extbase\Error\Error the target type, or an error object if a user-error occurred
* @throws \TYPO3\CMS\Extbase\Property\Exception\TypeConverterException thrown in case a developer error occurred
*/
public function convertFrom($source, string $targetType, array $convertedChildProperties = [], ?PropertyMappingConfigurationInterface $configuration = null);
}
+189
View File
@@ -0,0 +1,189 @@
<?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\Extbase\Property;
use TYPO3\CMS\Extbase\Utility\TypeHandlingUtility;
/**
* Registry containing all available Type Converters, registered via Services.yaml.
*
* @internal not part of TYPO3 Core API, as this is a framework-internal registry.
*/
class TypeConverterRegistry
{
/**
* @var array<string,array<string,array<int,TypeConverterInterface>>>
*/
protected array $typeConverters = [];
/**
* Used in the TypeConverterPass only.
*
* @param array|string[] $sources
* @throws Exception\DuplicateTypeConverterException
* @internal
*/
public function add(TypeConverterInterface $converter, int $priority, array $sources, string $target): void
{
foreach ($sources as $source) {
if (isset($this->typeConverters[$source][$target][$priority])) {
throw new Exception\DuplicateTypeConverterException(
sprintf(
'There exist at least two type converters which handle the conversion from "%s" to "%s" with priority "%d": %s and %s',
$source,
$target,
$priority,
get_class($this->typeConverters[$source][$target][$priority]),
get_class($converter)
),
1297951378
);
}
$this->typeConverters[$source][$target][$priority] = $converter;
}
}
/**
* @throws Exception\DuplicateTypeConverterException
* @throws Exception\InvalidTargetException
* @throws Exception\TypeConverterException
*/
public function findTypeConverter(string $sourceType, string $targetType): TypeConverterInterface
{
$converter = null;
if (TypeHandlingUtility::isSimpleType($targetType)) {
if (isset($this->typeConverters[$sourceType][$targetType])) {
$converter = $this->findEligibleConverterWithHighestPriority($this->typeConverters[$sourceType][$targetType]);
}
} else {
$converter = $this->findFirstEligibleTypeConverterInObjectHierarchy($sourceType, $targetType);
}
if ($converter === null) {
throw new Exception\TypeConverterException(
'No converter found which can be used to convert from "' . $sourceType . '" to "' . $targetType . '".',
1476044883
);
}
return $converter;
}
/**
* Tries to find a suitable type converter for the given source type and target type.
*
* @param string $sourceType Type of the source to convert from
* @param class-string $targetClass Name of the target class to find a type converter for
*
*
* @throws Exception\InvalidTargetException
* @throws Exception\DuplicateTypeConverterException
*/
protected function findFirstEligibleTypeConverterInObjectHierarchy(string $sourceType, string $targetClass): ?TypeConverterInterface
{
if (!class_exists($targetClass) && !interface_exists($targetClass)) {
throw new Exception\InvalidTargetException('Could not find a suitable type converter for "' . $targetClass . '" because no such class or interface exists.', 1297948764);
}
if (!isset($this->typeConverters[$sourceType])) {
return null;
}
$convertersForSource = $this->typeConverters[$sourceType];
if (isset($convertersForSource[$targetClass])) {
$converter = $this->findEligibleConverterWithHighestPriority($convertersForSource[$targetClass]);
if ($converter !== null) {
return $converter;
}
}
foreach (class_parents($targetClass) as $parentClass) {
if (!isset($convertersForSource[$parentClass])) {
continue;
}
$converter = $this->findEligibleConverterWithHighestPriority($convertersForSource[$parentClass]);
if ($converter !== null) {
return $converter;
}
}
$implementedInterface = class_implements($targetClass);
/** @var array<class-string,class-string> $implementedInterface */
$implementedInterface = $implementedInterface === false ? [] : $implementedInterface;
$implementedInterface = array_keys($implementedInterface);
$converters = $this->getConvertersForInterfaces($convertersForSource, $implementedInterface);
$converter = $this->findEligibleConverterWithHighestPriority($converters);
if ($converter !== null) {
return $converter;
}
if (isset($convertersForSource['object'])) {
return $this->findEligibleConverterWithHighestPriority($convertersForSource['object']);
}
return null;
}
/**
* @param array<int,TypeConverterInterface> $converters
*/
protected function findEligibleConverterWithHighestPriority(array $converters): ?TypeConverterInterface
{
if ($converters === []) {
return null;
}
krsort($converters, SORT_NUMERIC);
reset($converters);
return current($converters);
}
/**
* @param array<string,array<int,TypeConverterInterface>> $convertersForSource
* @param class-string[] $interfaceNames
*
* @return TypeConverterInterface[]
*
* @throws Exception\DuplicateTypeConverterException
*/
protected function getConvertersForInterfaces(array $convertersForSource, array $interfaceNames): array
{
$convertersForInterface = [];
foreach ($interfaceNames as $implementedInterface) {
if (isset($convertersForSource[$implementedInterface])) {
foreach ($convertersForSource[$implementedInterface] as $priority => $converter) {
if (isset($convertersForInterface[$priority])) {
throw new Exception\DuplicateTypeConverterException(
sprintf(
'There exist at least two converters which handle the conversion to an interface with priority "%d". %s and %s',
$priority,
get_class($convertersForInterface[$priority]),
get_class($converter)
),
1297951338
);
}
$convertersForInterface[$priority] = $converter;
}
}
}
return $convertersForInterface;
}
}