Skip to content

Commit

Permalink
feat: add IterableMapperInterface for mapping iterables (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
priyadi committed Apr 27, 2024
1 parent 42772cc commit c30d661
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## 1.2.0

* feat: add `IterableMapperInterface` for mapping iterables

## 1.1.2

* fix: property info caching if another bundle is decorating cache ([#47](https://github.com/rekalogika/mapper/issues/47))
Expand Down
5 changes: 4 additions & 1 deletion config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Rekalogika\Mapper\CustomMapper\Implementation\PropertyMapperResolver;
use Rekalogika\Mapper\CustomMapper\Implementation\WarmableObjectMapperTableFactory;
use Rekalogika\Mapper\Implementation\Mapper;
use Rekalogika\Mapper\IterableMapperInterface;
use Rekalogika\Mapper\MainTransformer\Implementation\MainTransformer;
use Rekalogika\Mapper\MapperInterface;
use Rekalogika\Mapper\Mapping\Implementation\MappingCacheWarmer;
Expand Down Expand Up @@ -64,7 +65,6 @@
use Rekalogika\Mapper\TypeResolver\Implementation\TypeResolver;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyInfo\PropertyInfoCacheExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
Expand Down Expand Up @@ -452,6 +452,9 @@
$services
->alias(MapperInterface::class, 'rekalogika.mapper.mapper');

$services
->alias(IterableMapperInterface::class, 'rekalogika.mapper.mapper');

# console command

$services
Expand Down
45 changes: 44 additions & 1 deletion src/Implementation/Mapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@

use Rekalogika\Mapper\Context\Context;
use Rekalogika\Mapper\Exception\UnexpectedValueException;
use Rekalogika\Mapper\IterableMapperInterface;
use Rekalogika\Mapper\MainTransformer\MainTransformerInterface;
use Rekalogika\Mapper\MapperInterface;
use Rekalogika\Mapper\Util\TypeFactory;

/**
* @internal
*/
final readonly class Mapper implements MapperInterface
final readonly class Mapper implements MapperInterface, IterableMapperInterface
{
public function __construct(
private MainTransformerInterface $transformer,
Expand All @@ -38,12 +39,14 @@ public function map(object $source, object|string $target, ?Context $context = n
{
if (is_string($target)) {
$targetClass = $target;

if (
!class_exists($targetClass)
&& !\interface_exists($targetClass)
) {
throw new UnexpectedValueException(sprintf('The target class "%s" does not exist.', $targetClass));
}

$targetType = TypeFactory::objectOfClass($targetClass);
$target = null;
} else {
Expand Down Expand Up @@ -71,4 +74,44 @@ public function map(object $source, object|string $target, ?Context $context = n

return $target;
}

/**
* @template T of object
* @param iterable<mixed> $source
* @param class-string<T> $target
* @return iterable<T>
*/
public function mapIterable(
iterable $source,
string $target,
?Context $context = null
): iterable {
$targetClass = $target;

if (
!class_exists($targetClass)
&& !\interface_exists($targetClass)
) {
throw new UnexpectedValueException(sprintf('The target class "%s" does not exist.', $targetClass));
}

$targetType = TypeFactory::objectOfClass($targetClass);

/** @var mixed $item */
foreach ($source as $item) {
$result = $this->transformer->transform(
source: $item,
target: null,
sourceType: null,
targetTypes: [$targetType],
context: $context ?? Context::create(),
);

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

yield $result;
}
}
}
27 changes: 27 additions & 0 deletions src/IterableMapperInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?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;

use Rekalogika\Mapper\Context\Context;

interface IterableMapperInterface
{
/**
* @template T of object
* @param iterable<mixed> $source
* @param class-string<T> $target
* @return iterable<T>
*/
public function mapIterable(iterable $source, string $target, ?Context $context = null): iterable;
}
8 changes: 8 additions & 0 deletions tests/Common/FrameworkTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Rekalogika\Mapper\Context\Context;
use Rekalogika\Mapper\Debug\MapperDataCollector;
use Rekalogika\Mapper\Debug\TraceableTransformer;
use Rekalogika\Mapper\IterableMapperInterface;
use Rekalogika\Mapper\MapperInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
Expand All @@ -30,6 +31,8 @@ abstract class FrameworkTestCase extends TestCase
private ContainerInterface $container;
/** @psalm-suppress MissingConstructor */
protected MapperInterface $mapper;
/** @psalm-suppress MissingConstructor */
protected IterableMapperInterface $iterableMapper;

public function setUp(): void
{
Expand All @@ -41,6 +44,11 @@ public function setUp(): void
$this->get(MapperInterface::class),
$this->getMapperContext()
);

$this->iterableMapper = new IterableMapperDecorator(
$this->get(IterableMapperInterface::class),
$this->getMapperContext()
);
}

/**
Expand Down
31 changes: 31 additions & 0 deletions tests/Common/IterableMapperDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?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 Rekalogika\Mapper\Context\Context;
use Rekalogika\Mapper\IterableMapperInterface;

final class IterableMapperDecorator implements IterableMapperInterface
{
public function __construct(
private IterableMapperInterface $decorated,
private Context $defaultContext
) {
}

public function mapIterable(iterable $source, string $target, ?Context $context = null): iterable
{
return $this->decorated->mapIterable($source, $target, $context ?? $this->defaultContext);
}
}
2 changes: 2 additions & 0 deletions tests/Common/TestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace Rekalogika\Mapper\Tests\Common;

use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
use Rekalogika\Mapper\IterableMapperInterface;
use Rekalogika\Mapper\MapperInterface;
use Rekalogika\Mapper\Mapping\MappingFactoryInterface;
use Rekalogika\Mapper\RekalogikaMapperBundle;
Expand Down Expand Up @@ -69,6 +70,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void
public static function getServiceIds(): iterable
{
yield MapperInterface::class;
yield IterableMapperInterface::class;

yield 'rekalogika.mapper.property_info';
// yield 'rekalogika.mapper.cache.property_info';
Expand Down
33 changes: 33 additions & 0 deletions tests/Fixtures/Basic/Person.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?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\Fixtures\Basic;

class Person
{
public function __construct(
private string $name,
private int $age
) {
}

public function getName(): string
{
return $this->name;
}

public function getAge(): int
{
return $this->age;
}
}
20 changes: 20 additions & 0 deletions tests/Fixtures/Basic/PersonDto.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?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\Fixtures\Basic;

class PersonDto
{
public ?string $name = null;
public ?int $age = null;
}
49 changes: 49 additions & 0 deletions tests/IntegrationTest/IterableMapperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?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\IntegrationTest;

use Rekalogika\Mapper\Tests\Common\FrameworkTestCase;
use Rekalogika\Mapper\Tests\Fixtures\Basic\Person;
use Rekalogika\Mapper\Tests\Fixtures\Basic\PersonDto;

class IterableMapperTest extends FrameworkTestCase
{
public function testAdder(): void
{
$result = $this->iterableMapper->mapIterable($this->getIterableInput(), PersonDto::class);
/** @psalm-suppress InvalidArgument */
$result = iterator_to_array($result);

$this->assertCount(3, $result);
$this->assertInstanceOf(PersonDto::class, $result[0]);
$this->assertInstanceOf(PersonDto::class, $result[1]);
$this->assertInstanceOf(PersonDto::class, $result[2]);
$this->assertSame('John Doe', $result[0]->name);
$this->assertSame(30, $result[0]->age);
$this->assertSame('Jane Doe', $result[1]->name);
$this->assertSame(25, $result[1]->age);
$this->assertSame('Foo Bar', $result[2]->name);
$this->assertSame(99, $result[2]->age);
}

/**
* @return iterable<Person>
*/
private function getIterableInput(): iterable
{
yield new Person('John Doe', 30);
yield new Person('Jane Doe', 25);
yield new Person('Foo Bar', 99);
}
}

0 comments on commit c30d661

Please sign in to comment.