Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add IterableMapperInterface for mapping iterables #53

Merged
merged 1 commit into from
Apr 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
Loading