Skip to content

Commit

Permalink
Merge branch 'feature/interface-mapping'
Browse files Browse the repository at this point in the history
  • Loading branch information
mark-gerarts committed Apr 6, 2019
2 parents f3dac4a + 114f8e7 commit fd62a84
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 11 deletions.
19 changes: 14 additions & 5 deletions src/AutoMapper.php
Expand Up @@ -71,9 +71,19 @@ public function map($source, string $destinationClass, array $context = [])
return $this->getCustomMapper($mapping)->map($source, $destinationClass);
}

$destinationObject = $mapping->hasCustomConstructor()
? $mapping->getCustomConstructor()($source, $this, $context)
: new $destinationClass;
if ($mapping->hasCustomConstructor()) {
$destinationObject = $mapping->getCustomConstructor()($source, $this, $context);
}
elseif (interface_exists($destinationClass)) {
// If we're mapping to an interface a valid custom constructor has
// to be provided. Otherwise we can't know what to do.
$message = 'Mapping to an interface is not possible. Please '
. 'provide a concrete class or use mapToObject instead.';
throw new AutoMapperPlusException($message);
}
else {
$destinationObject = new $destinationClass;
}

return $this->doMap($source, $destinationObject, $mapping, $context);
}
Expand Down Expand Up @@ -169,8 +179,7 @@ protected function getMapping
(
string $sourceClass,
string $destinationClass
): MappingInterface
{
): MappingInterface {
$mapping = $this->autoMapperConfig->getMappingFor(
$sourceClass,
$destinationClass
Expand Down
24 changes: 18 additions & 6 deletions src/Configuration/AutoMapperConfig.php
Expand Up @@ -81,7 +81,7 @@ public function getMappingFor(
// mapping from it.
$candidates = array_filter(
$this->mappings,
function (MappingInterface $mapping) use ($sourceClassName, $destinationClassName) {
function (MappingInterface $mapping) use ($sourceClassName, $destinationClassName): bool {
return is_a($sourceClassName, $mapping->getSourceClassName(), true)
&& is_a($destinationClassName, $mapping->getDestinationClassName(), true);
}
Expand Down Expand Up @@ -153,8 +153,7 @@ protected function getMostSpecificCandidate(
*/
protected function getClassDistance(
string $childClass,
string
$parentClass
string $parentClass
): int {
if ($childClass === $parentClass) {
return 0;
Expand All @@ -169,10 +168,23 @@ protected function getClassDistance(
}
}

// We'll treat implementing an interface as having a greater class
// distance. This because we want a concrete implementation to be more
// specific than an interface. For example, suppose we have:
// - FooInterface
// - FooClass, implementing the above interface
// If a mapping has been registered for both of these, we want the
// mapper to pick the mapping registered for FooClass, since this is
// more specific.
$interfaces = class_implements($childClass);
if (\in_array($parentClass, $interfaces, true)) {
return ++$result;
}

// @todo: use a domain specific exception.
throw new \Exception("
This error should have never be thrown.
This could only happen, if given childClass is not a child of the given parentClass"
throw new \Exception(
'This error should have never be thrown.
This could only happen if given childClass is not a child of the given parentClass'
);
}

Expand Down
44 changes: 44 additions & 0 deletions test/Configuration/AutoMapperConfigTest.php
Expand Up @@ -7,6 +7,10 @@
use AutoMapperPlus\Test\Models\Inheritance\DestinationParent;
use AutoMapperPlus\Test\Models\Inheritance\SourceChild;
use AutoMapperPlus\Test\Models\Inheritance\SourceParent;
use AutoMapperPlus\Test\Models\Interfaces\DestinationImplementation;
use AutoMapperPlus\Test\Models\Interfaces\DestinationInterface;
use AutoMapperPlus\Test\Models\Interfaces\SourceImplementation;
use AutoMapperPlus\Test\Models\Interfaces\SourceInterface;
use PHPUnit\Framework\TestCase;
use AutoMapperPlus\Test\Models\SimpleProperties\Destination;
use AutoMapperPlus\Test\Models\SimpleProperties\Source;
Expand Down Expand Up @@ -81,4 +85,44 @@ public function testMappingsGetGeneratedOnTheFlyIfOptionSet()
Destination::class
));
}

public function testInterfacesAreLessSpecificThanClassesInTheSource()
{
$config = new AutoMapperConfig();
$config->registerMapping(
SourceInterface::class,
DestinationImplementation::class
);
$concreteMapping = $config->registerMapping(
SourceImplementation::class,
DestinationImplementation::class
);

$result = $config->getMappingFor(
SourceImplementation::class,
DestinationImplementation::class
);

$this->assertEquals($concreteMapping, $result);
}

public function testInterfacesAreLessSpecificThanClassesInTheDestination()
{
$config = new AutoMapperConfig();
$config->registerMapping(
SourceImplementation::class,
DestinationInterface::class
);
$concreteMapping = $config->registerMapping(
SourceImplementation::class,
DestinationImplementation::class
);

$result = $config->getMappingFor(
SourceImplementation::class,
DestinationImplementation::class
);

$this->assertEquals($concreteMapping, $result);
}
}
8 changes: 8 additions & 0 deletions test/Models/Interfaces/DestinationImplementation.php
@@ -0,0 +1,8 @@
<?php

namespace AutoMapperPlus\Test\Models\Interfaces;

class DestinationImplementation implements DestinationInterface
{
public $name;
}
8 changes: 8 additions & 0 deletions test/Models/Interfaces/DestinationInterface.php
@@ -0,0 +1,8 @@
<?php

namespace AutoMapperPlus\Test\Models\Interfaces;

interface DestinationInterface
{

}
18 changes: 18 additions & 0 deletions test/Models/Interfaces/SourceImplementation.php
@@ -0,0 +1,18 @@
<?php

namespace AutoMapperPlus\Test\Models\Interfaces;

class SourceImplementation implements SourceInterface
{
private $name;

public function __construct(string $name)
{
$this->name = $name;
}

public function getName(): string
{
return $this->name;
}
}
13 changes: 13 additions & 0 deletions test/Models/Interfaces/SourceInterface.php
@@ -0,0 +1,13 @@
<?php

namespace AutoMapperPlus\Test\Models\Interfaces;

/**
* Interface SourceInterface
*
* @package AutoMapperPlus\Test\Models\Interfaces
*/
interface SourceInterface
{
public function getName(): string;
}
54 changes: 54 additions & 0 deletions test/Scenarios/InterfacesTest.php
@@ -0,0 +1,54 @@
<?php

namespace AutoMapperPlus\Test\Scenarios;

use AutoMapperPlus\AutoMapper;
use AutoMapperPlus\Configuration\AutoMapperConfig;
use AutoMapperPlus\Exception\AutoMapperPlusException;
use AutoMapperPlus\Test\Models\Interfaces\DestinationImplementation;
use AutoMapperPlus\Test\Models\Interfaces\DestinationInterface;
use AutoMapperPlus\Test\Models\Interfaces\SourceImplementation;
use AutoMapperPlus\Test\Models\Interfaces\SourceInterface;
use PHPUnit\Framework\TestCase;

class InterfacesTest extends TestCase
{
public function testItMapsFromAnInterface()
{
$config = new AutoMapperConfig();
$config->registerMapping(SourceInterface::class, DestinationImplementation::class);
$mapper = new AutoMapper($config);
$source = new SourceImplementation('a name');

$result = $mapper->map($source, DestinationImplementation::class);

$this->assertEquals('a name', $result->name);
}

public function testItDoesntAllowMappingToAnInterface()
{
$this->expectException(AutoMapperPlusException::class);

$config = new AutoMapperConfig();
$config->registerMapping(SourceImplementation::class, DestinationInterface::class)
->dontSkipConstructor();
$mapper = new AutoMapper($config);
$source = new SourceImplementation('a name');

$result = $mapper->map($source, DestinationInterface::class);

$this->assertEquals('a name', $result->name);
}

public function testMappingToAnInterfaceIsAllowedForMapToObject()
{
$config = new AutoMapperConfig();
$config->registerMapping(SourceImplementation::class, DestinationInterface::class);
$mapper = new AutoMapper($config);
$source = new SourceImplementation('a name');
$destination = new DestinationImplementation();
$result = $mapper->mapToObject($source, $destination);

$this->assertEquals('a name', $result->name);
}
}

0 comments on commit fd62a84

Please sign in to comment.