Skip to content

Commit c30d661

Browse files
authored
feat: add IterableMapperInterface for mapping iterables (#53)
1 parent 42772cc commit c30d661

10 files changed

+222
-2
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# CHANGELOG
22

3+
## 1.2.0
4+
5+
* feat: add `IterableMapperInterface` for mapping iterables
6+
37
## 1.1.2
48

59
* fix: property info caching if another bundle is decorating cache ([#47](https://github.com/rekalogika/mapper/issues/47))

config/services.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Rekalogika\Mapper\CustomMapper\Implementation\PropertyMapperResolver;
2121
use Rekalogika\Mapper\CustomMapper\Implementation\WarmableObjectMapperTableFactory;
2222
use Rekalogika\Mapper\Implementation\Mapper;
23+
use Rekalogika\Mapper\IterableMapperInterface;
2324
use Rekalogika\Mapper\MainTransformer\Implementation\MainTransformer;
2425
use Rekalogika\Mapper\MapperInterface;
2526
use Rekalogika\Mapper\Mapping\Implementation\MappingCacheWarmer;
@@ -64,7 +65,6 @@
6465
use Rekalogika\Mapper\TypeResolver\Implementation\TypeResolver;
6566
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
6667
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
67-
use Symfony\Component\PropertyInfo\PropertyInfoCacheExtractor;
6868
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
6969
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
7070
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
@@ -452,6 +452,9 @@
452452
$services
453453
->alias(MapperInterface::class, 'rekalogika.mapper.mapper');
454454

455+
$services
456+
->alias(IterableMapperInterface::class, 'rekalogika.mapper.mapper');
457+
455458
# console command
456459

457460
$services

src/Implementation/Mapper.php

+44-1
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515

1616
use Rekalogika\Mapper\Context\Context;
1717
use Rekalogika\Mapper\Exception\UnexpectedValueException;
18+
use Rekalogika\Mapper\IterableMapperInterface;
1819
use Rekalogika\Mapper\MainTransformer\MainTransformerInterface;
1920
use Rekalogika\Mapper\MapperInterface;
2021
use Rekalogika\Mapper\Util\TypeFactory;
2122

2223
/**
2324
* @internal
2425
*/
25-
final readonly class Mapper implements MapperInterface
26+
final readonly class Mapper implements MapperInterface, IterableMapperInterface
2627
{
2728
public function __construct(
2829
private MainTransformerInterface $transformer,
@@ -38,12 +39,14 @@ public function map(object $source, object|string $target, ?Context $context = n
3839
{
3940
if (is_string($target)) {
4041
$targetClass = $target;
42+
4143
if (
4244
!class_exists($targetClass)
4345
&& !\interface_exists($targetClass)
4446
) {
4547
throw new UnexpectedValueException(sprintf('The target class "%s" does not exist.', $targetClass));
4648
}
49+
4750
$targetType = TypeFactory::objectOfClass($targetClass);
4851
$target = null;
4952
} else {
@@ -71,4 +74,44 @@ public function map(object $source, object|string $target, ?Context $context = n
7174

7275
return $target;
7376
}
77+
78+
/**
79+
* @template T of object
80+
* @param iterable<mixed> $source
81+
* @param class-string<T> $target
82+
* @return iterable<T>
83+
*/
84+
public function mapIterable(
85+
iterable $source,
86+
string $target,
87+
?Context $context = null
88+
): iterable {
89+
$targetClass = $target;
90+
91+
if (
92+
!class_exists($targetClass)
93+
&& !\interface_exists($targetClass)
94+
) {
95+
throw new UnexpectedValueException(sprintf('The target class "%s" does not exist.', $targetClass));
96+
}
97+
98+
$targetType = TypeFactory::objectOfClass($targetClass);
99+
100+
/** @var mixed $item */
101+
foreach ($source as $item) {
102+
$result = $this->transformer->transform(
103+
source: $item,
104+
target: null,
105+
sourceType: null,
106+
targetTypes: [$targetType],
107+
context: $context ?? Context::create(),
108+
);
109+
110+
if (!is_object($result) || !is_a($result, $target)) {
111+
throw new UnexpectedValueException(sprintf('The mapper did not return the variable of expected class, expecting "%s", returned "%s".', $targetClass, get_debug_type($target)));
112+
}
113+
114+
yield $result;
115+
}
116+
}
74117
}

src/IterableMapperInterface.php

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of rekalogika/mapper package.
7+
*
8+
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
9+
*
10+
* For the full copyright and license information, please view the LICENSE file
11+
* that was distributed with this source code.
12+
*/
13+
14+
namespace Rekalogika\Mapper;
15+
16+
use Rekalogika\Mapper\Context\Context;
17+
18+
interface IterableMapperInterface
19+
{
20+
/**
21+
* @template T of object
22+
* @param iterable<mixed> $source
23+
* @param class-string<T> $target
24+
* @return iterable<T>
25+
*/
26+
public function mapIterable(iterable $source, string $target, ?Context $context = null): iterable;
27+
}

tests/Common/FrameworkTestCase.php

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Rekalogika\Mapper\Context\Context;
2121
use Rekalogika\Mapper\Debug\MapperDataCollector;
2222
use Rekalogika\Mapper\Debug\TraceableTransformer;
23+
use Rekalogika\Mapper\IterableMapperInterface;
2324
use Rekalogika\Mapper\MapperInterface;
2425
use Symfony\Component\DependencyInjection\ContainerInterface;
2526
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
@@ -30,6 +31,8 @@ abstract class FrameworkTestCase extends TestCase
3031
private ContainerInterface $container;
3132
/** @psalm-suppress MissingConstructor */
3233
protected MapperInterface $mapper;
34+
/** @psalm-suppress MissingConstructor */
35+
protected IterableMapperInterface $iterableMapper;
3336

3437
public function setUp(): void
3538
{
@@ -41,6 +44,11 @@ public function setUp(): void
4144
$this->get(MapperInterface::class),
4245
$this->getMapperContext()
4346
);
47+
48+
$this->iterableMapper = new IterableMapperDecorator(
49+
$this->get(IterableMapperInterface::class),
50+
$this->getMapperContext()
51+
);
4452
}
4553

4654
/**
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of rekalogika/mapper package.
7+
*
8+
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
9+
*
10+
* For the full copyright and license information, please view the LICENSE file
11+
* that was distributed with this source code.
12+
*/
13+
14+
namespace Rekalogika\Mapper\Tests\Common;
15+
16+
use Rekalogika\Mapper\Context\Context;
17+
use Rekalogika\Mapper\IterableMapperInterface;
18+
19+
final class IterableMapperDecorator implements IterableMapperInterface
20+
{
21+
public function __construct(
22+
private IterableMapperInterface $decorated,
23+
private Context $defaultContext
24+
) {
25+
}
26+
27+
public function mapIterable(iterable $source, string $target, ?Context $context = null): iterable
28+
{
29+
return $this->decorated->mapIterable($source, $target, $context ?? $this->defaultContext);
30+
}
31+
}

tests/Common/TestKernel.php

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace Rekalogika\Mapper\Tests\Common;
1515

1616
use Doctrine\Bundle\DoctrineBundle\DoctrineBundle;
17+
use Rekalogika\Mapper\IterableMapperInterface;
1718
use Rekalogika\Mapper\MapperInterface;
1819
use Rekalogika\Mapper\Mapping\MappingFactoryInterface;
1920
use Rekalogika\Mapper\RekalogikaMapperBundle;
@@ -69,6 +70,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void
6970
public static function getServiceIds(): iterable
7071
{
7172
yield MapperInterface::class;
73+
yield IterableMapperInterface::class;
7274

7375
yield 'rekalogika.mapper.property_info';
7476
// yield 'rekalogika.mapper.cache.property_info';

tests/Fixtures/Basic/Person.php

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of rekalogika/mapper package.
7+
*
8+
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
9+
*
10+
* For the full copyright and license information, please view the LICENSE file
11+
* that was distributed with this source code.
12+
*/
13+
14+
namespace Rekalogika\Mapper\Tests\Fixtures\Basic;
15+
16+
class Person
17+
{
18+
public function __construct(
19+
private string $name,
20+
private int $age
21+
) {
22+
}
23+
24+
public function getName(): string
25+
{
26+
return $this->name;
27+
}
28+
29+
public function getAge(): int
30+
{
31+
return $this->age;
32+
}
33+
}

tests/Fixtures/Basic/PersonDto.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of rekalogika/mapper package.
7+
*
8+
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
9+
*
10+
* For the full copyright and license information, please view the LICENSE file
11+
* that was distributed with this source code.
12+
*/
13+
14+
namespace Rekalogika\Mapper\Tests\Fixtures\Basic;
15+
16+
class PersonDto
17+
{
18+
public ?string $name = null;
19+
public ?int $age = null;
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of rekalogika/mapper package.
7+
*
8+
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
9+
*
10+
* For the full copyright and license information, please view the LICENSE file
11+
* that was distributed with this source code.
12+
*/
13+
14+
namespace Rekalogika\Mapper\Tests\IntegrationTest;
15+
16+
use Rekalogika\Mapper\Tests\Common\FrameworkTestCase;
17+
use Rekalogika\Mapper\Tests\Fixtures\Basic\Person;
18+
use Rekalogika\Mapper\Tests\Fixtures\Basic\PersonDto;
19+
20+
class IterableMapperTest extends FrameworkTestCase
21+
{
22+
public function testAdder(): void
23+
{
24+
$result = $this->iterableMapper->mapIterable($this->getIterableInput(), PersonDto::class);
25+
/** @psalm-suppress InvalidArgument */
26+
$result = iterator_to_array($result);
27+
28+
$this->assertCount(3, $result);
29+
$this->assertInstanceOf(PersonDto::class, $result[0]);
30+
$this->assertInstanceOf(PersonDto::class, $result[1]);
31+
$this->assertInstanceOf(PersonDto::class, $result[2]);
32+
$this->assertSame('John Doe', $result[0]->name);
33+
$this->assertSame(30, $result[0]->age);
34+
$this->assertSame('Jane Doe', $result[1]->name);
35+
$this->assertSame(25, $result[1]->age);
36+
$this->assertSame('Foo Bar', $result[2]->name);
37+
$this->assertSame(99, $result[2]->age);
38+
}
39+
40+
/**
41+
* @return iterable<Person>
42+
*/
43+
private function getIterableInput(): iterable
44+
{
45+
yield new Person('John Doe', 30);
46+
yield new Person('Jane Doe', 25);
47+
yield new Person('Foo Bar', 99);
48+
}
49+
}

0 commit comments

Comments
 (0)