automapper
automapper copied to clipboard
Make better discriminator
I try us mapper with symfony discriminator, but it's not what i expected, For example
// I have Source Interface or Abstract class
interface SourceInterface
{
}
/// And his impl
final readonly class SourceImpl implements SourceInterface
{
}
/// And i have target class for mapping, its Interface or Abstract too
interface TargetInterface
{
}
final readonly class TargetImpl implements TargetInterface
{
}
In usage something like
$source = new Source();
// For interface
$result = $this->mapper->map($source, TargetInterface::class);
// And for abstract
$result = $this->mapper->map($source, AbstractTarget::class, (new MapperContext())->setConstructorArgument(AbstractTarget::class, 'property', 'value')->toArray();
I try imp with event listener and replace target
#[\Attribute(\Attribute::TARGET_CLASS)]
final readonly class Discriminator
{
/**
* @param class-string $baseClass
* @param array<class-string, class-string> $map
*/
public function __construct(
public string $baseClass,
public array $map,
) { }
}
#[AsEventListener(GenerateMapperEvent::class)]
final readonly class DiscriminatorListener
{
public function __invoke(GenerateMapperEvent $event): void
{
if (!$event->mapperMetadata->targetReflectionClass) {
return;
}
$attributes = $event->mapperMetadata
->targetReflectionClass
->getAttributes(Discriminator::class);
if (count($attributes) === 0) {
return;
}
foreach ($attributes as $attr) {
$discriminatorAttribute = $attr->newInstance();
$baseClassRef = new ReflectionClass($discriminatorAttribute->baseClass);
if (!$event->mapperMetadata->sourceReflectionClass->isSubclassOf($baseClassRef)) {
continue;
}
if (!$baseClassRef->isInterface() && !$baseClassRef->isAbstract()) {
throw new BadMapDefinitionException(sprintf(
'Required `baseClass` should be abstract or interface in "%s" attribute on "%s" class.',
Discriminator::class,
$event->mapperMetadata->targetReflectionClass->getName(),
));
}
foreach ($discriminatorAttribute->map as $source => $target) {
$ref = new ReflectionClass($source);
if (!$ref->isSubclassOf($baseClassRef->getName())) {
throw new BadMapDefinitionException(sprintf(
'Required value of `map` should be subclass of %s in "%s" attribute on "%s" class.',
$baseClassRef->getName(),
Discriminator::class,
$event->mapperMetadata->targetReflectionClass->getName(),
));
}
if ($ref->getName() === $event->mapperMetadata->sourceReflectionClass->getName()) {
$event->mapperMetadata->target = $target;
}
}
}
}
}
And it's work, but i still can't get default construct argument from context, because it's by from context by class name
RFC for example
interface SourceInterface
{
}
final readonly class SourceImpl implements SourceInterface
{
}
#[Discriminator(
SourceInterface::class, //Base class for source
[
SourceImpl::class => TargetImpl::class, // In mapping get check source class and his mapping
]
)]
interface TargetInterface
{
}
final readonly class TargetImpl implements TargetInterface
{
}
You are right current discriminator approach is more based on a field value rather than a class object, our current approach is more focused on a mapping from or to an array, we could certainly do better when it's between two object models.
PR welcomed, however this PR should also handle the 'array' behavior to allow using this attribute without symfony also