Skip to content

Commit

Permalink
feat: Method mapper
Browse files Browse the repository at this point in the history
  • Loading branch information
priyadi committed Jan 12, 2024
1 parent 7f7657c commit 6a2be97
Show file tree
Hide file tree
Showing 14 changed files with 721 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
`Countable` result.
* docs: Improve documentation
* fix: Change `ObjectCache` to use `WeakMap`. Should improve memory usage.
* feat: Method mapper

## 0.5.3

Expand Down
22 changes: 20 additions & 2 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use Rekalogika\Mapper\Mapping\CachingMappingFactory;
use Rekalogika\Mapper\Mapping\MappingFactory;
use Rekalogika\Mapper\Mapping\MappingFactoryInterface;
use Rekalogika\Mapper\MethodMapper\ClassMethodTransformer;
use Rekalogika\Mapper\MethodMapper\SubMapper;
use Rekalogika\Mapper\ObjectCache\ObjectCacheFactory;
use Rekalogika\Mapper\Transformer\ArrayToObjectTransformer;
use Rekalogika\Mapper\Transformer\CopyTransformer;
Expand Down Expand Up @@ -84,14 +86,22 @@

$services
->set('rekalogika.mapper.transformer.scalar_to_scalar', ScalarToScalarTransformer::class)
->tag('rekalogika.mapper.transformer', ['priority' => -500]);
->tag('rekalogika.mapper.transformer', ['priority' => -450]);

$services
->set('rekalogika.mapper.transformer.datetime', DateTimeTransformer::class)
->tag('rekalogika.mapper.transformer', ['priority' => -550]);
->tag('rekalogika.mapper.transformer', ['priority' => -500]);

$services
->set('rekalogika.mapper.transformer.string_to_backed_enum', StringToBackedEnumTransformer::class)
->tag('rekalogika.mapper.transformer', ['priority' => -550]);

$services
->set('rekalogika.mapper.method_mapper.transformer', ClassMethodTransformer::class)
->args([
service('rekalogika.mapper.method_mapper.sub_mapper'),
service('rekalogika.mapper.object_cache_factory'),
])
->tag('rekalogika.mapper.transformer', ['priority' => -600]);

$services
Expand Down Expand Up @@ -176,6 +186,14 @@
service('rekalogika.mapper.type_resolver.caching.inner'),
]);

# method mapper

$services
->set('rekalogika.mapper.method_mapper.sub_mapper', SubMapper::class)
->args([
service('rekalogika.mapper.property_info'),
]);

# other services

$services
Expand Down
30 changes: 30 additions & 0 deletions src/MapperFactory/MapperFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
use Rekalogika\Mapper\MapperInterface;
use Rekalogika\Mapper\Mapping\MappingFactory;
use Rekalogika\Mapper\Mapping\MappingFactoryInterface;
use Rekalogika\Mapper\MethodMapper\ClassMethodTransformer;
use Rekalogika\Mapper\MethodMapper\SubMapper;
use Rekalogika\Mapper\MethodMapper\SubMapperInterface;
use Rekalogika\Mapper\ObjectCache\ObjectCacheFactory;
use Rekalogika\Mapper\ObjectCache\ObjectCacheFactoryInterface;
use Rekalogika\Mapper\Transformer\ArrayToObjectTransformer;
Expand Down Expand Up @@ -82,6 +85,7 @@ class MapperFactory
private ?TraversableToArrayAccessTransformer $traversableToArrayAccessTransformer = null;
private ?TraversableToTraversableTransformer $traversableToTraversableTransformer = null;
private ?CopyTransformer $copyTransformer = null;
private ?ClassMethodTransformer $classMethodTransformer = null;

private CacheItemPoolInterface $propertyInfoExtractorCache;
private null|(PropertyInfoExtractorInterface&PropertyInitializableExtractorInterface) $propertyInfoExtractor = null;
Expand All @@ -90,6 +94,7 @@ class MapperFactory
private ?MapperInterface $mapper = null;
private ?MappingFactoryInterface $mappingFactory = null;
private ?ObjectCacheFactoryInterface $objectCacheFactory = null;
private ?SubMapper $subMapper = null;

private ?MappingCommand $mappingCommand = null;
private ?TryCommand $tryCommand = null;
Expand Down Expand Up @@ -365,6 +370,18 @@ protected function getCopyTransformer(): TransformerInterface
return $this->copyTransformer;
}

protected function getClassMethodTransformer(): ClassMethodTransformer
{
if (null === $this->classMethodTransformer) {
$this->classMethodTransformer = new ClassMethodTransformer(
$this->getSubMapper(),
$this->getObjectCacheFactory(),
);
}

return $this->classMethodTransformer;
}

//
// other services
//
Expand All @@ -390,6 +407,8 @@ protected function getTransformersIterator(): iterable
=> $this->getDateTimeTransformer();
yield 'StringToBackedEnumTransformer'
=> $this->getStringToBackedEnumTransformer();
yield 'ClassMethodTransformer'
=> $this->getClassMethodTransformer();
yield 'ObjectToStringTransformer'
=> $this->getObjectToStringTransformer();
yield 'TraversableToArrayAccessTransformer'
Expand Down Expand Up @@ -449,6 +468,17 @@ protected function getObjectCacheFactory(): ObjectCacheFactoryInterface
return $this->objectCacheFactory;
}

protected function getSubMapper(): SubMapper
{
if (null === $this->subMapper) {
$this->subMapper = new SubMapper(
$this->getPropertyInfoExtractor(),
);
}

return $this->subMapper;
}

//
// command
//
Expand Down
119 changes: 119 additions & 0 deletions src/MethodMapper/ClassMethodTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?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\MethodMapper;

use Rekalogika\Mapper\Contracts\MainTransformerAwareInterface;
use Rekalogika\Mapper\Contracts\MainTransformerAwareTrait;
use Rekalogika\Mapper\Contracts\TransformerInterface;
use Rekalogika\Mapper\Contracts\TypeMapping;
use Rekalogika\Mapper\Exception\InvalidArgumentException;
use Rekalogika\Mapper\MainTransformer;
use Rekalogika\Mapper\ObjectCache\ObjectCache;
use Rekalogika\Mapper\ObjectCache\ObjectCacheFactoryInterface;
use Rekalogika\Mapper\Util\TypeFactory;
use Symfony\Component\PropertyInfo\Type;

final class ClassMethodTransformer implements
TransformerInterface,
MainTransformerAwareInterface
{
use MainTransformerAwareTrait;

public function __construct(
private SubMapper $subMapper,
private ObjectCacheFactoryInterface $objectCacheFactory,
) {
}

public function transform(
mixed $source,
mixed $target,
Type $sourceType,
?Type $targetType,
array $context
): mixed {
// target type must not be null

if ($targetType === null) {
throw new InvalidArgumentException('Target type must not be null.');
}

// prepare subMapper

$subMapper = $this->subMapper->withMainTransformer($this->getMainTransformer());

// target class must be valid

$targetClass = $targetType->getClassName();

if (
!is_string($targetClass)
|| !\class_exists($targetClass)
) {
throw new InvalidArgumentException(sprintf('Target class "%s" is not a valid class.', (string) $targetClass));
}


if (is_a($targetClass, MapFromObjectInterface::class, true)) {

// map from object to self path

if (!is_object($source)) {
throw new InvalidArgumentException(sprintf('Source must be object, "%s" given', get_debug_type($source)));
}

$result = $targetClass::mapFromObject($source, $subMapper, $context);
} elseif ($source instanceof MapToObjectInterface) {

// map self to object path

if (!is_object($target)) {
$target = $targetClass;
}

$result = $source->mapToObject($target, $subMapper, $context);
} else {
throw new \LogicException('Should not reach here');
}

// get object cache

if (!isset($context[MainTransformer::OBJECT_CACHE])) {
$objectCache = $this->objectCacheFactory->createObjectCache();
$context[MainTransformer::OBJECT_CACHE] = $objectCache;
} else {
/** @var ObjectCache */
$objectCache = $context[MainTransformer::OBJECT_CACHE];
}

// save to object cache

$objectCache->saveTarget($source, $targetType, $target);

return $result;
}

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

yield new TypeMapping(
TypeFactory::object(),
TypeFactory::objectOfClass(MapFromObjectInterface::class),
);
}
}
26 changes: 26 additions & 0 deletions src/MethodMapper/MapFromObjectInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\MethodMapper;

interface MapFromObjectInterface
{
/**
* @param array<string,mixed> $context
*/
public static function mapFromObject(
object $source,
SubMapperInterface $mapper,
array $context = []
): static;
}
26 changes: 26 additions & 0 deletions src/MethodMapper/MapToObjectInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\MethodMapper;

interface MapToObjectInterface
{
/**
* @param array<string,mixed> $context
*/
public function mapToObject(
object|string $target,
SubMapperInterface $mapper,
array $context = []
): object;
}
92 changes: 92 additions & 0 deletions src/MethodMapper/SubMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?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\MethodMapper;

use Rekalogika\Mapper\Contracts\MainTransformerAwareInterface;
use Rekalogika\Mapper\Contracts\MainTransformerAwareTrait;
use Rekalogika\Mapper\Exception\UnexpectedValueException;
use Rekalogika\Mapper\Util\TypeFactory;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\Type;

/**
* Specialized mapper used in MethodMapper.
*/
class SubMapper implements SubMapperInterface, MainTransformerAwareInterface
{
use MainTransformerAwareTrait;

public function __construct(
private PropertyTypeExtractorInterface $propertyTypeExtractor,
) {
}

public function map(
object $source,
object|string $target,
array $context = []
): object {
if (is_object($target)) {
$targetClass = $target::class;
$targetObject = $target;
} else {
$targetClass = $target;
$targetObject = null;
}

/** @var mixed */
$result = $this->getMainTransformer()->transform(
$source,
$targetObject,
[TypeFactory::objectOfClass($targetClass)],
$context
);

if (is_object($target)) {
$targetClass = $target::class;
} else {
$targetClass = $target;
}

if ($result instanceof $targetClass) {
return $result;
}

throw new UnexpectedValueException(sprintf('The mapper did not return the variable of expected class, expecting "%s", returned "%s".', $targetClass, get_debug_type($target)));
}

public function mapForProperty(
object $source,
string $class,
string $property,
array $context = []
): mixed {
/** @var array<int,Type>|null */
$targetPropertyTypes = $this->propertyTypeExtractor->getTypes(
$class,
$property,
$context
);

/** @var mixed */
$result = $this->getMainTransformer()->transform(
$source,
null,
$targetPropertyTypes ?? [],
$context
);

return $result;
}
}
Loading

0 comments on commit 6a2be97

Please sign in to comment.