jolicode / jolicode/automapper
feature proposal: NoOp transformer
- Dominant language
- PHP
- Stars
- 217
- Forks
- 26
- Avg merge
- 1h 35m
- Merged PRs (30d)
- 2
Description
Hi,
thank you for your work,
by default, automapper creates a clone of mapped objects properties.
This is an issue when mapping doctrine entities (typical use case: mapping an entity with one to many relationships to a symfony form model). The mapped entity object isn't managed by doctrine.
Using the `MaxDepth` attribute might bes an option in some cases, but doesn't allow fine control of the mapped values.
This feature proposal introduces a "No Operation" transformer that plop the original object property value in the target class property.
## Implementation proposal
Here is a possible (and working) but probably incomplete implementation
A `NoOp` attribute to identify properties to map as is (without recursion):
```php
getSourceUniqueType();
if (!$sourceUniqueType instanceof Type) {
return false;
}
$targetUniqueType = $types->getTargetUniqueType($sourceUniqueType);
if (!$targetUniqueType instanceof Type) {
return false;
}
$isSameClass = $this->isSameClass($sourceUniqueType, $targetUniqueType);
$hasSourceNoOpAttribute = $this->hasNoOpAttribute($source, $mapperMetadata);
$hasTargetNoOpAttribute = $this->hasNoOpAttribute($target, $mapperMetadata);
return $isSameClass && ($hasSourceNoOpAttribute || $hasTargetNoOpAttribute);
}
#[\Override]
public function transform(mixed $value, object|array $source, array $context): mixed
{
return $value;
}
private function isSameClass(Type $source, Type $target): bool
{
$sourceClassName = $source->getClassName();
$targetClassName = $target->getClassName();
return $sourceClassName === $targetClassName
&& null !== $sourceClassName
&& null !== $targetClassName;
}
private function hasNoOpAttribute(
SourcePropertyMetadata|TargetPropertyMetadata $propertyMetadata,
MapperMetadata $mapperMetadata,
): bool {
$propertyAttributes = $this->getMappedPropertyAttributes($propertyMetadata, $mapperMetadata);
foreach ($propertyAttributes as $attribute) {
if (NoOp::class === $attribute->getName()) {
return true;
}
}
return false;
}
/**
* @return \ReflectionAttribute[]
*/
private function getMappedPropertyAttributes(
SourcePropertyMetadata|TargetPropertyMetadata $propertyMetadata,
MapperMetadata $mapperMetadata,
): array {
$propertyName = $propertyMetadata->property;
$reflectionClass = match (true) {
$propertyMetadata instanceof SourcePropertyMetadata => $mapperMetadata->sourceReflectionClass,
$propertyMetadata instanceof TargetPropertyMetadata => $mapperMetadata->targetReflectionClass,
default => throw new \LogicException('Invalid property metadata type'),
};
if (!$reflectionClass instanceof \ReflectionClass) {
return [];
}
return $this->getPropertyAttributes($reflectionClass, $propertyName);
}
/**
* @return \ReflectionAttribute[]
*/
private function getPropertyAttributes(
\ReflectionClass $reflectionClass,
string $propertyName,
): array {
$reflectionProperty = $this->getReflectionProperty($reflectionClass, $propertyName);
if ($reflectionProperty instanceof \ReflectionProperty) {
return $reflectionProperty->getAttributes();
}
return [];
}
private function getReflectionProperty(
\ReflectionClass $reflectionClass,
string $propertyName,
): ?\ReflectionProperty {
if (!$reflectionClass->hasProperty($propertyName)) {
return null;
}
return $reflectionClass->getProperty($propertyName);
}
}
```
## Usage
Consider a `BlogPost` entity with a mandatory `Category` property:
```php
Contributor guide
Assessment
This issue has not been assessed yet.