automapper icon indicating copy to clipboard operation
automapper copied to clipboard

Make better discriminator

Open Norbytus opened this issue 1 year ago • 1 comments

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
{
}

Norbytus avatar Oct 09 '24 14:10 Norbytus

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

joelwurtz avatar Mar 07 '25 09:03 joelwurtz