Skip to content

Commit

Permalink
feat(PropertyMapper): Option to add Context & `MainTransformerInt…
Browse files Browse the repository at this point in the history
…erface` as extra arguments to the property mapper.
  • Loading branch information
priyadi committed Feb 3, 2024
1 parent 68f5293 commit 2ed9644
Show file tree
Hide file tree
Showing 15 changed files with 327 additions and 169 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
* refactor(`PropertyMapper`): If `property` is missing, use the method name,
stripping the leading 'map' and lowercasing the first letter. Example:
`mapName` will map to the property `name`.
* feat(`PropertyMapper`): Option to add `Context` & `MainTransformerInterface`
as extra arguments to the property mapper.

## 0.5.26

Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ Full documentation is available at [rekalogika.dev/mapper](https://rekalogika.de
## Future Features

* Option to read & write to private properties.
* Option to inject `Context` and `MainTransformer` to a property mapper.
* Data collector and profiler integration.

## Installation
Expand Down
36 changes: 12 additions & 24 deletions config/tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,29 @@
use Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\PropertyMapperWithClassAttributeWithoutExplicitProperty;
use Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\PropertyMapperWithConstructorWithClassAttribute;
use Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\PropertyMapperWithConstructorWithoutClassAttribute;
use Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\PropertyMapperWithExtraArguments;
use Rekalogika\Mapper\Tests\Fixtures\PropertyMapper\PropertyMapperWithoutClassAttribute;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();

$services->defaults()
->autowire()
->autoconfigure()
->public();

// add test aliases
$serviceIds = TestKernel::getServiceIds();

foreach ($serviceIds as $serviceId) {
$services->alias('test.' . $serviceId, $serviceId)->public();
};

$services->set(PropertyMapperWithoutClassAttribute::class)
->autowire()
->autoconfigure()
->public();

$services->set(PropertyMapperWithClassAttribute::class)
->autowire()
->autoconfigure()
->public();

$services->set(PropertyMapperWithConstructorWithoutClassAttribute::class)
->autowire()
->autoconfigure()
->public();

$services->set(PropertyMapperWithConstructorWithClassAttribute::class)
->autowire()
->autoconfigure()
->public();

$services->set(PropertyMapperWithClassAttributeWithoutExplicitProperty::class)
->autowire()
->autoconfigure()
->public();
$services->set(PropertyMapperWithoutClassAttribute::class);
$services->set(PropertyMapperWithClassAttribute::class);
$services->set(PropertyMapperWithConstructorWithoutClassAttribute::class);
$services->set(PropertyMapperWithConstructorWithClassAttribute::class);
$services->set(PropertyMapperWithClassAttributeWithoutExplicitProperty::class);
$services->set(PropertyMapperWithExtraArguments::class);
};
61 changes: 59 additions & 2 deletions src/DependencyInjection/PropertyMapperPass.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@

namespace Rekalogika\Mapper\DependencyInjection;

use Rekalogika\Mapper\Context\Context;
use Rekalogika\Mapper\Exception\InvalidArgumentException;
use Rekalogika\Mapper\MainTransformer\MainTransformerInterface;
use Rekalogika\Mapper\PropertyMapper\Contracts\PropertyMapperServicePointer;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

Expand All @@ -23,20 +27,73 @@ public function process(ContainerBuilder $container)
$propertyMapperResolver = $container
->getDefinition('rekalogika.mapper.property_mapper.resolver');

foreach ($container->findTaggedServiceIds('rekalogika.mapper.property_mapper') as $serviceId => $tags) {
$taggedServices = $container->findTaggedServiceIds('rekalogika.mapper.property_mapper');

foreach ($taggedServices as $serviceId => $tags) {
$serviceDefinition = $container->getDefinition($serviceId);
$serviceClass = $serviceDefinition->getClass() ?? throw new InvalidArgumentException('Class is required');

/** @var array<string,string> $tag */
foreach ($tags as $tag) {
$method = $tag['method'] ?? throw new InvalidArgumentException('Method is required');

$propertyMapperResolver->addMethodCall(
'addPropertyMapper',
[
$tag['sourceClass'],
$tag['targetClass'],
$tag['property'],
$serviceId,
$tag['method'],
$method,
self::getExtraArguments($serviceClass, $method),
]
);
}
}
}

/**
* @param class-string $serviceClass
* @return array<int,PropertyMapperServicePointer::ARGUMENT_*>
*/
private static function getExtraArguments(
string $serviceClass,
string $method
): array {
$reflectionClass = new \ReflectionClass($serviceClass);
$parameters = $reflectionClass->getMethod($method)->getParameters();
// remove first element, which is always the source class
array_shift($parameters);

$extraArguments = [];

foreach ($parameters as $parameter) {
$type = $parameter->getType();

if (!$type instanceof \ReflectionNamedType) {
throw new InvalidArgumentException(
sprintf(
'Extra arguments for property mapper "%s" in class "%s" must be type hinted.',
$method,
$serviceClass,
)
);
}

$extraArguments[] = match ($type->getName()) {
Context::class => PropertyMapperServicePointer::ARGUMENT_CONTEXT,
MainTransformerInterface::class => PropertyMapperServicePointer::ARGUMENT_MAIN_TRANSFORMER,
default => throw new InvalidArgumentException(
sprintf(
'Extra argument with type "%s" for property mapper "%s" in class "%s" is unsupported.',
$type->getName(),
$method,
$serviceClass,
)
)
};
}

return $extraArguments;
}
}
9 changes: 7 additions & 2 deletions src/MapperFactory/MapperFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use Rekalogika\Mapper\ObjectCache\ObjectCacheFactory;
use Rekalogika\Mapper\ObjectCache\ObjectCacheFactoryInterface;
use Rekalogika\Mapper\PropertyMapper\Contracts\PropertyMapperResolverInterface;
use Rekalogika\Mapper\PropertyMapper\Contracts\PropertyMapperServicePointer;
use Rekalogika\Mapper\PropertyMapper\PropertyMapperResolver;
use Rekalogika\Mapper\Transformer\ArrayLikeMetadata\ArrayLikeMetadataFactory;
use Rekalogika\Mapper\Transformer\ArrayLikeMetadata\Contracts\ArrayLikeMetadataFactoryInterface;
Expand Down Expand Up @@ -81,7 +82,7 @@
class MapperFactory
{
/**
* @var array<int,array{sourceClass:class-string,targetClass:class-string,property:string,service:object,method:string}>
* @var array<int,array{sourceClass:class-string,targetClass:class-string,property:string,service:object,method:string,extraArguments:array<int,PropertyMapperServicePointer::ARGUMENT_*>}>
*/
private array $propertyMappers = [];

Expand Down Expand Up @@ -139,20 +140,23 @@ public function __construct(
/**
* @param class-string $sourceClass
* @param class-string $targetClass
* @param array<int,PropertyMapperServicePointer::ARGUMENT_*> $extraArguments
*/
public function addPropertyMapper(
string $sourceClass,
string $targetClass,
string $property,
object $service,
string $method
string $method,
array $extraArguments = []
): void {
$this->propertyMappers[] = [
'sourceClass' => $sourceClass,
'targetClass' => $targetClass,
'property' => $property,
'service' => $service,
'method' => $method,
'extraArguments' => $extraArguments,
];
}

Expand Down Expand Up @@ -559,6 +563,7 @@ protected function getPropertyMapperResolver(): PropertyMapperResolverInterface
$propertyMapper['property'],
$propertyMapper['service']::class,
$propertyMapper['method'],
$propertyMapper['extraArguments'],
);
}
}
Expand Down
12 changes: 0 additions & 12 deletions src/PropertyMapper/Contracts/PropertyMapperResolverInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,6 @@

interface PropertyMapperResolverInterface
{
/**
* @param class-string $sourceClass
* @param class-string $targetClass
*/
public function addPropertyMapper(
string $sourceClass,
string $targetClass,
string $property,
string $serviceId,
string $method
): void;

/**
* @param class-string $sourceClass
* @param class-string $targetClass
Expand Down
15 changes: 15 additions & 0 deletions src/PropertyMapper/Contracts/PropertyMapperServicePointer.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@

final readonly class PropertyMapperServicePointer
{
public const ARGUMENT_CONTEXT = 'context';
public const ARGUMENT_MAIN_TRANSFORMER = 'main_transformer';

/**
* @param array<int,self::ARGUMENT_*> $extraArguments
*/
public function __construct(
private string $serviceId,
private string $method,
private array $extraArguments,
) {
}

Expand All @@ -30,4 +37,12 @@ public function getMethod(): string
{
return $this->method;
}

/**
* @return array<int,self::ARGUMENT_*>
*/
public function getExtraArguments(): array
{
return $this->extraArguments;
}
}
6 changes: 4 additions & 2 deletions src/PropertyMapper/PropertyMapperResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@ class PropertyMapperResolver implements PropertyMapperResolverInterface
/**
* @param class-string $sourceClass
* @param class-string $targetClass
* @param array<int,PropertyMapperServicePointer::ARGUMENT_*> $extraArguments
*/
public function addPropertyMapper(
string $sourceClass,
string $targetClass,
string $property,
string $serviceId,
string $method
string $method,
array $extraArguments = []
): void {
$this->propertyMappers[$targetClass][$property][$sourceClass]
= new PropertyMapperServicePointer($serviceId, $method);
= new PropertyMapperServicePointer($serviceId, $method, $extraArguments);
}

/**
Expand Down
13 changes: 12 additions & 1 deletion src/Transformer/ObjectToObjectTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Rekalogika\Mapper\Context\Context;
use Rekalogika\Mapper\Exception\InvalidArgumentException;
use Rekalogika\Mapper\ObjectCache\ObjectCache;
use Rekalogika\Mapper\PropertyMapper\Contracts\PropertyMapperServicePointer;
use Rekalogika\Mapper\Transformer\Contracts\MainTransformerAwareInterface;
use Rekalogika\Mapper\Transformer\Contracts\MainTransformerAwareTrait;
use Rekalogika\Mapper\Transformer\Contracts\TransformerInterface;
Expand Down Expand Up @@ -255,12 +256,22 @@ private function transformValue(
$propertyMapper = $this->propertyMapperLocator
->get($propertyMapperPointer->getServiceId());

$extraArguments = $propertyMapperPointer->getExtraArguments();
$extraArgumentsParams = [];

foreach ($extraArguments as $extraArgument) {
$extraArgumentsParams[] = match ($extraArgument) {
PropertyMapperServicePointer::ARGUMENT_CONTEXT => $context,
PropertyMapperServicePointer::ARGUMENT_MAIN_TRANSFORMER => $this->getMainTransformer(),
};
}

/**
* @psalm-suppress MixedAssignment
* @psalm-suppress MixedMethodCall
*/
$targetPropertyValue = $propertyMapper->{$propertyMapperPointer
->getMethod()}($source);
->getMethod()}($source, ...$extraArgumentsParams);

/** @psalm-suppress MixedAssignment */
return $targetPropertyValue;
Expand Down
59 changes: 59 additions & 0 deletions tests/Common/AbstractFrameworkTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?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\Tests\Common;

use PHPUnit\Framework\TestCase;
use Rekalogika\Mapper\MapperInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;

abstract class AbstractFrameworkTest extends TestCase
{
private ContainerInterface $container;
/** @psalm-suppress MissingConstructor */
protected MapperInterface $mapper;

public function setUp(): void
{
$kernel = new TestKernel();
$kernel->boot();
$this->container = $kernel->getContainer();
$this->mapper = $this->get(MapperInterface::class);
}

/**
* @template T of object
* @param string|class-string<T> $serviceId
* @return ($serviceId is class-string<T> ? T : object)
*/
public function get(string $serviceId): object
{
try {
$result = $this->container->get('test.' . $serviceId);
} catch (ServiceNotFoundException) {
/** @psalm-suppress PossiblyNullReference */
$result = $this->container->get($serviceId);
}


if (class_exists($serviceId) || interface_exists($serviceId)) {
$this->assertInstanceOf($serviceId, $result);
}

/** @psalm-suppress RedundantConditionGivenDocblockType */
$this->assertNotNull($result);

return $result;
}
}
3 changes: 3 additions & 0 deletions tests/Common/TestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace Rekalogika\Mapper\Tests\Common;

use Rekalogika\Mapper\MapperInterface;
use Rekalogika\Mapper\Mapping\MappingFactoryInterface;
use Rekalogika\Mapper\RekalogikaMapperBundle;
use Rekalogika\Mapper\Transformer\ArrayToObjectTransformer;
Expand Down Expand Up @@ -69,6 +70,8 @@ public function registerContainerConfiguration(LoaderInterface $loader): void
*/
public static function getServiceIds(): iterable
{
yield MapperInterface::class;

yield 'rekalogika.mapper.property_info';
yield 'rekalogika.mapper.cache.property_info';
yield 'rekalogika.mapper.property_info.cache';
Expand Down
Loading

0 comments on commit 2ed9644

Please sign in to comment.