Skip to content

Commit

Permalink
reverse inheritance wip
Browse files Browse the repository at this point in the history
  • Loading branch information
priyadi committed Jan 15, 2024
1 parent 0f15c7e commit 494cf01
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 40 deletions.
9 changes: 0 additions & 9 deletions src/Attribute/InheritanceMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,4 @@ public function getMap(): array
{
return $this->map;
}

/**
* @param class-string $sourceClass
* @return class-string|null
*/
public function getTargetClassFromSourceClass(string $sourceClass): ?string
{
return $this->map[$sourceClass] ?? null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@

namespace Rekalogika\Mapper\Transformer\Exception;

use Rekalogika\Mapper\Attribute\InheritanceMap;
use Rekalogika\Mapper\Context\Context;

class SourceClassNotInInheritanceMapException extends NotMappableValueException
class ClassNotInInheritanceMapException extends NotMappableValueException
{
/**
* @param class-string $sourceClass
Expand All @@ -29,9 +28,9 @@ public function __construct(
) {
parent::__construct(
message: sprintf(
'Trying to map to a class with an inheritance map, but source class "%s" is not found in the "%s" attribute of the target class "%s".',
'Trying to map source class "%s" to target class "%s" using an inheritance map, but the target class "%s" is missing from the inheritance map.',
$sourceClass,
InheritanceMap::class,
$targetClass,
$targetClass
),
context: $context,
Expand Down
95 changes: 82 additions & 13 deletions src/Transformer/InheritanceMapTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
use Rekalogika\Mapper\Transformer\Contracts\MainTransformerAwareTrait;
use Rekalogika\Mapper\Transformer\Contracts\TransformerInterface;
use Rekalogika\Mapper\Transformer\Contracts\TypeMapping;
use Rekalogika\Mapper\Transformer\Exception\SourceClassNotInInheritanceMapException;
use Rekalogika\Mapper\Transformer\Exception\ClassNotInInheritanceMapException;
use Rekalogika\Mapper\Util\AttributeUtil;
use Rekalogika\Mapper\Util\TypeFactory;
use Symfony\Component\PropertyInfo\Type;

Expand Down Expand Up @@ -54,10 +55,24 @@ public function transform(
throw new InvalidArgumentException('Target type must be specified.', context: $context);
}

# target must be an interface or class
# source and target must be an interface or class

$sourceClass = $targetType->getClassName();
$targetClass = $targetType->getClassName();

if (
$sourceClass === null
|| (
!\class_exists($sourceClass)
&& !\interface_exists($sourceClass)
)
) {
throw new InvalidArgumentException(
\sprintf('Source class "%s" does not exist.', $sourceClass ?? 'null'),
context: $context
);
}

if (
$targetClass === null
|| (
Expand All @@ -73,25 +88,29 @@ public function transform(

# gets the inheritance map

$attributes = (new \ReflectionClass($targetClass))
->getAttributes(InheritanceMap::class);
$inheritanceMap = $this->getMapFromTargetClass($targetClass);

if (\count($attributes) === 0) {
throw new InvalidArgumentException(
\sprintf('Target class "%s" must have inheritance map.', $targetClass),
context: $context
);
}

$inheritanceMap = $attributes[0]->newInstance();
if ($inheritanceMap === null) {
$inheritanceMap = $this->getMapFromSourceClass($sourceClass);

if ($inheritanceMap === null) {
throw new InvalidArgumentException(
\sprintf('Either source class "%s" or target class "%s" must have inheritance map.', $sourceClass, $targetClass),
context: $context
);
}

$inheritanceMap = \array_flip($inheritanceMap);
}

# gets the target class from the inheritance map

$sourceClass = \get_class($source);
$targetClassInMap = $inheritanceMap->getTargetClassFromSourceClass($sourceClass);
$targetClassInMap = $inheritanceMap[$sourceClass] ?? null;

if ($targetClassInMap === null) {
throw new SourceClassNotInInheritanceMapException($sourceClass, $targetClass);
throw new ClassNotInInheritanceMapException($sourceClass, $targetClass);
}

# pass the transformation back to the main transformer
Expand All @@ -117,12 +136,62 @@ public function transform(
return $result;
}

/**
* @param class-string $class
* @return null|array<class-string,class-string>
*/
private function getMapFromTargetClass(string $class): array|null
{
$attributes = AttributeUtil::getAttributes(new \ReflectionClass($class));

foreach ($attributes as $attribute) {
if ($attribute->getName() !== InheritanceMap::class) {
continue;
}

/** @var InheritanceMap $inheritanceMap */
$inheritanceMap = $attribute->newInstance();

return $inheritanceMap->getMap();
}

return null;
}

/**
* @param class-string $class
* @return null|array<class-string,class-string>
*/
private function getMapFromSourceClass(string $class): array|null
{
$attributes = AttributeUtil::getAttributesIncludingParents(new \ReflectionClass($class));

foreach ($attributes as $attribute) {
if ($attribute->getName() !== InheritanceMap::class) {
continue;
}

/** @var InheritanceMap $inheritanceMap */
$inheritanceMap = $attribute->newInstance();

return array_flip($inheritanceMap->getMap());
}

return null;
}

public function getSupportedTransformation(): iterable
{
yield new TypeMapping(
TypeFactory::object(),
TypeFactory::objectOfClass(InheritanceMap::class),
true
);

yield new TypeMapping(
TypeFactory::objectOfClass(InheritanceMap::class),
TypeFactory::object(),
true
);
}
}
86 changes: 86 additions & 0 deletions src/Util/AttributeUtil.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

declare(strict_types=1);

/*
* This file is part of rekalogika/mapper package.
*
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/

namespace Rekalogika\Mapper\Util;

use Rekalogika\Mapper\Attribute\MapperAttributeInterface;

class AttributeUtil
{
/**
* @var array<class-string,array<int,\ReflectionAttribute<MapperAttributeInterface>>>
*/
private static array $attributesIncludingParentsCache = [];

/**
* @param \ReflectionClass<object> $class
* @return array<int,\ReflectionAttribute<MapperAttributeInterface>>
*/
public static function getAttributesIncludingParents(\ReflectionClass $class): array
{
$className = $class->getName();

if (isset(self::$attributesIncludingParentsCache[$className])) {
return self::$attributesIncludingParentsCache[$className];
}

$attributes = [];

while ($class !== false) {
$attributes = array_merge(
$attributes,
self::getAttributes($class)
);

$class = $class->getParentClass();
}

foreach (class_implements($className) as $interface) {
$interface = new \ReflectionClass($interface);

$attributes = array_merge(
$attributes,
self::getAttributes($interface)
);
}

return self::$attributesIncludingParentsCache[$className] = $attributes;
}

/**
* @var array<class-string,array<int,\ReflectionAttribute<MapperAttributeInterface>>>
*/
private static array $attributes = [];

/**
* @param \ReflectionClass<object> $class
* @return array<int,\ReflectionAttribute<MapperAttributeInterface>>
*/
public static function getAttributes(\ReflectionClass $class): array
{
$className = $class->getName();

if (isset(self::$attributes[$className])) {
return self::$attributes[$className];
}

$attributes = [];

$attributes = $class->getAttributes(
MapperAttributeInterface::class,
\ReflectionAttribute::IS_INSTANCEOF
);

return self::$attributes[$className] = $attributes;
}
}
19 changes: 8 additions & 11 deletions src/Util/TypeUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -376,17 +376,14 @@ private static function getAttributesFromType(
return [];
}

$attributes = (new \ReflectionClass($class))
->getAttributes(
MapperAttributeInterface::class,
\ReflectionAttribute::IS_INSTANCEOF
);

$attributeTypes = [];

foreach ($attributes as $attribute) {
$attributeTypes[] = TypeFactory::objectOfClass($attribute->getName());
}
$reflectionClass = new \ReflectionClass($class);
$attributes = AttributeUtil::getAttributesIncludingParents($reflectionClass);

$attributeTypes = array_map(
fn (\ReflectionAttribute $attribute)
=> TypeFactory::objectOfClass($attribute->getName()),
$attributes
);

return $attributeTypes;
}
Expand Down
14 changes: 11 additions & 3 deletions tests/IntegrationTest/InheritanceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Rekalogika\Mapper\Tests\IntegrationTest;

use Rekalogika\Mapper\Tests\Common\AbstractIntegrationTest;
use Rekalogika\Mapper\Tests\Fixtures\Inheritance\AbstractClass;
use Rekalogika\Mapper\Tests\Fixtures\Inheritance\ConcreteClassA;
use Rekalogika\Mapper\Tests\Fixtures\Inheritance\ConcreteClassC;
use Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\AbstractClassDto;
Expand All @@ -22,9 +23,9 @@
use Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\ImplementationADto;
use Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\InterfaceDto;
use Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\InterfaceWithoutMapDto;
use Rekalogika\Mapper\Transformer\Exception\ClassNotInInheritanceMapException;
use Rekalogika\Mapper\Transformer\Exception\ClassNotInstantiableException;
use Rekalogika\Mapper\Transformer\Exception\NotAClassException;
use Rekalogika\Mapper\Transformer\Exception\SourceClassNotInInheritanceMapException;

class InheritanceTest extends AbstractIntegrationTest
{
Expand All @@ -38,6 +39,13 @@ public function testMapToAbstractClass(): void
$this->assertInstanceOf(ConcreteClassADto::class, $result);
$this->assertSame('propertyInA', $result->propertyInA);
$this->assertSame('propertyInParent', $result->propertyInParent);

// map back

$result = $this->mapper->map($result, AbstractClass::class);
$this->assertInstanceOf(ConcreteClassA::class, $result);
$this->assertSame('propertyInA', $result->propertyInA);
$this->assertSame('propertyInParent', $result->propertyInParent);
}

public function testMapToAbstractClassWithoutMap(): void
Expand All @@ -50,7 +58,7 @@ public function testMapToAbstractClassWithoutMap(): void
public function testMapToAbstractClassWithMissingSourceClassInMap(): void
{
$concreteClassC = new ConcreteClassC();
$this->expectException(SourceClassNotInInheritanceMapException::class);
$this->expectException(ClassNotInInheritanceMapException::class);
$result = $this->mapper->map($concreteClassC, AbstractClassDto::class);
}

Expand All @@ -75,7 +83,7 @@ public function testMapToInterfaceWithoutMap(): void
public function testMapToInterfaceWithMissingSourceClassInMap(): void
{
$concreteClassC = new ConcreteClassC();
$this->expectException(SourceClassNotInInheritanceMapException::class);
$this->expectException(ClassNotInInheritanceMapException::class);
$result = $this->mapper->map($concreteClassC, InterfaceDto::class);
}
}

0 comments on commit 494cf01

Please sign in to comment.