mark-gerarts / automapper-plus

An AutoMapper for PHP
MIT License
551 stars 30 forks source link

QUESTION : Asserting complete mapping #86

Open brendanjerwin opened 7 months ago

brendanjerwin commented 7 months ago

A feature of the dotnet AutoMapper is AssertConfigurationIsValid() which will barf if a new property was added that was not able to be automagically mapped.

I'm trying to find a way to assert the same here. A unit test that will fail if there is a destination or source property that doesn't have an operation.

How might I go about this? Am I missing something obvious already?

mark-gerarts commented 7 months ago

Hi @brendanjerwin, this is a good question. It is not part of the library right now.

Do you want every property to have an explicit operation defined, or is it also considered valid if mapping is possible with the default operation (i.e. mapping properties with the same name)? If the former, you could hack it in yourself using a bit of reflection:

<?php

require 'vendor/autoload.php';

use AutoMapperPlus\Configuration\AutoMapperConfig;
use AutoMapperPlus\AutoMapper;
use AutoMapperPlus\MappingOperation\Operation;
use AutoMapperPlus\Configuration\MappingInterface;
use AutoMapperPlus\MappingOperation\DefaultMappingOperation;

class CustomConfig extends AutoMapperConfig
{
    private $configuredMappings = [];

    public function registerMapping(
        string $sourceClassName,
        string $destinationClassName
    ): MappingInterface {
        $this->configuredMappings[] = [$sourceClassName, $destinationClassName];
        return parent::registerMapping($sourceClassName, $destinationClassName);
    }

    public function assertValid(): void {
        foreach ($this->configuredMappings as [$sourceClassName, $destinationClassName]) {
            $mapping = $this->getMappingFor($sourceClassName, $destinationClassName);

            $sourceObject = (new \ReflectionClass($sourceClassName))->newInstance();
            $destinationObject = (new \ReflectionClass($destinationClassName))->newInstance();

            $properties = $mapping->getTargetProperties($destinationObject, $sourceObject);
            foreach ($properties as $property) {
                $operation = $mapping->getMappingOperationFor($property);
                if ($operation instanceof DefaultMappingOperation) {
                    throw new \Exception("${destinationClassName}::${property} has no explicit mapping operation defined");
                }
            }
        }
    }
}

class ClassA
{
    public $prop1 = 'property 1';
    public $prop2 = 'property 2';
}

class ClassB
{
    public $prop1;
    public $prop2;
}

$config = new CustomConfig();
$config
    ->registerMapping(ClassA::class, ClassB::class)
    ->forMember('prop1', Operation::setTo('hardcoded prop'));
$mapper = new AutoMapper($config);

$config->assertValid();

The latter can also be done with some additions to the code above. I call it hacking, since the public API of the library doesn't expose which mappings are actually registered.

If fleshed out a bit, it could be a good feature to actually implement in the library.