, non-empty-string|int> */ protected array $configuration = []; /** * Stores the configuration for specific child properties. * * @var array */ protected array $subConfigurationForProperty = []; /** * Keys which should be renamed * * @var array */ protected array $mapping = []; protected ?TypeConverterInterface $typeConverter = null; /** * List of allowed property names to be converted * * @var array */ protected array $propertiesToBeMapped = []; /** * List of property names to be skipped during property mapping * * @var array */ protected array $propertiesToSkip = []; /** * List of disallowed property names which will be ignored while property mapping * * @var array */ 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 $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 $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 $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 $typeConverter The type converter class * @return array 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; } }