Skip to content

Make better discriminator #196

@Norbytus

Description

@Norbytus

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions