FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(mapper): serialize nested objects by radoslav-grencik · Pull Request #2259 · tempestphp/tempest-framework · GitHub

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .php  (7) All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
69 changes: 51 additions & 18 deletions packages/mapper/src/Mappers/ObjectToArrayMapper.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
use Tempest\Reflection\ClassReflector;
use Tempest\Reflection\PropertyReflector;

use function Tempest\Mapper\map;

final readonly class ObjectToArrayMapper implements Mapper
{
public function __construct(
Expand All @@ -30,32 +28,63 @@ public function canMap(mixed $from, mixed $to): bool

public function map(mixed $from, mixed $to): mixed
{
if ($from instanceof JsonSerializable) {
return $from->jsonSerialize();
$visited = [];

return $this->mapValue($from, $visited);
}

/**
* @param array<int, true> $visited
*/
private function mapValue(mixed $value, array &$visited): mixed
{
if ($value instanceof JsonSerializable) {
return $value->jsonSerialize();
}

if (is_object($from)) {
$class = new ClassReflector($from);
if (! is_object($value)) {
return $value;
}

$mappedProperties = [];
return $this->mapObject($value, $visited);
}

foreach ($class->getPublicProperties() as $property) {
if ($property->hasAttribute(Hidden::class)) {
continue;
}
/**
* @param array<int, true> $visited
*/
private function mapObject(object $object, array &$visited): mixed
{
$objectId = spl_object_id($object);

$propertyName = $this->resolvePropertyName($property);
$propertyValue = $this->resolvePropertyValue($property, $from);
$mappedProperties[$propertyName] = $propertyValue;
if (isset($visited[$objectId])) {
return $object;
}

$visited[$objectId] = true;

$class = new ClassReflector($object);

$mappedProperties = [];

foreach ($class->getPublicProperties() as $property) {
if ($property->hasAttribute(Hidden::class)) {
continue;
}
} else {
$mappedProperties = $from;

$propertyName = $this->resolvePropertyName($property);
$propertyValue = $this->resolvePropertyValue($property, $object, $visited);
$mappedProperties[$propertyName] = $propertyValue;
}

unset($visited[$objectId]);

return $mappedProperties;
}

private function resolvePropertyValue(PropertyReflector $property, object $object): mixed
/**
* @param array<int, true> $visited
*/
private function resolvePropertyValue(PropertyReflector $property, object $object, array &$visited): mixed
{
if (! $property->isInitialized($object)) {
return null;
Expand All @@ -69,7 +98,7 @@ private function resolvePropertyValue(PropertyReflector $property, object $objec
continue;
}

$propertyValue[$key] = map($value)->toArray();
$propertyValue[$key] = $this->mapValue($value, $visited);
}

return $propertyValue;
Expand All @@ -79,6 +108,10 @@ private function resolvePropertyValue(PropertyReflector $property, object $objec
return $serializer->serialize($propertyValue);
}

if ($propertyValue !== null && is_object($propertyValue)) {
return $this->mapValue($propertyValue, $visited);
}

return $propertyValue;
}

Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

namespace Tempest\Mapper\Serializers;

use Tempest\Mapper\ConfigurableSerializer;
use Tempest\Mapper\Context;
use Tempest\Mapper\DynamicSerializer;
use Tempest\Mapper\Exceptions\ValueCouldNotBeSerialized;
use Tempest\Mapper\Mappers\ObjectToArrayMapper;
use Tempest\Mapper\MappingContext;
use Tempest\Mapper\Serializer;
use Tempest\Reflection\PropertyReflector;
use Tempest\Reflection\TypeReflector;
Expand All @@ -15,8 +18,16 @@
use function Tempest\Mapper\map;

#[Priority(Priority::HIGHEST)]
final class ArrayOfObjectsSerializer implements Serializer, DynamicSerializer
final class ArrayOfObjectsSerializer implements Serializer, DynamicSerializer, ConfigurableSerializer
{
private readonly Context $context;

public function __construct(
?Context $context = null,
) {
$this->context = $context ?? MappingContext::default();
}

public static function accepts(PropertyReflector|TypeReflector $input): bool
{
if ($input instanceof TypeReflector) {
Expand All @@ -26,6 +37,11 @@ public static function accepts(PropertyReflector|TypeReflector $input): bool
return $input->getIterableType() instanceof TypeReflector;
}

public static function configure(PropertyReflector|TypeReflector|string $input, Context $context): Serializer
{
return new self($context);
}

public function serialize(mixed $input): array
{
if (! is_array($input)) {
Expand All @@ -35,7 +51,10 @@ public function serialize(mixed $input): array
$values = [];

foreach ($input as $key => $object) {
$values[$key] = map($object)->with(ObjectToArrayMapper::class)->do();
$values[$key] = map($object)
->in($this->context)
->with(ObjectToArrayMapper::class)
->do();
}

return $values;
Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Tests\Tempest\Integration\Mapper\Fixtures;

use Tempest\DateTime\DateTime;
use Tempest\DateTime\FormatPattern;
use Tempest\Validation\Rules\HasDateTimeFormat;

final readonly class NestedObjectWithDate
{
public function __construct(
#[HasDateTimeFormat(FormatPattern::ISO8601)]
public DateTime $createdAt,
) {}
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Tests\Tempest\Integration\Mapper\Fixtures;

use Tempest\DateTime\DateTime;
use Tempest\DateTime\FormatPattern;
use Tempest\Validation\Rules\HasDateTimeFormat;

final readonly class ObjectWithNestedObjectAndDate
{
public function __construct(
#[HasDateTimeFormat(FormatPattern::ISO8601)]
public DateTime $createdAt,
public NestedObjectWithDate $child,
/** @var \Tests\Tempest\Integration\Mapper\Fixtures\NestedObjectWithDate[] */
public array $children,
) {}
}
115 changes: 115 additions & 0 deletions tests/Integration/Mapper/Mappers/ObjectToArrayMapperTest.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@

namespace Tests\Tempest\Integration\Mapper\Mappers;

use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use PHPUnit\Framework\Attributes\Test;
use RuntimeException;
use Tempest\DateTime\DateTime;
use Tempest\Mapper\Mapper;
use Tempest\Mapper\MapperConfig;
use Tempest\Support\Json\Exception\JsonCouldNotBeEncoded;
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
use Tests\Tempest\Integration\Mapper\Fixtures\NestedObjectWithDate;
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectA;
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithJsonSerialize;
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithNestedObjectAndDate;
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithNullableProperties;
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithScalarValues;
use Tests\Tempest\Integration\Mapper\Fixtures\ParentObject;

use function Tempest\Mapper\map;

Expand Down Expand Up @@ -69,4 +78,110 @@ public function object_with_scalar_values_to_array(): void
$array,
);
}

#[Test]
public function object_with_single_nested_object_to_array(): void
{
$date = DateTime::parse('2026-08-19T12:34:56+00:00');

$array = map(new ObjectWithNestedObjectAndDate(
createdAt: $date,
child: new NestedObjectWithDate($date),
children: [new NestedObjectWithDate($date)],
))->toArray();

$this->assertSame(
[
'createdAt' => '2026-08-19T12:34:56.000Z',
'child' => [
'createdAt' => '2026-08-19T12:34:56.000Z',
],
'children' => [
['createdAt' => '2026-08-19T12:34:56.000Z'],
],
],
$array,
);
}

#[Test]
#[RequiresPhpExtension('pcntl')]
public function cyclic_nested_objects_fail_instead_of_hanging(): void
{
$parent = map([
'name' => 'parent',
'child' => ['name' => 'child'],
])->to(ParentObject::class);
$this->assertInstanceOf(ParentObject::class, $parent);

pcntl_async_signals(true);
pcntl_signal(SIGALRM, static function (): never {
throw new RuntimeException('Serialization did not terminate');
});
pcntl_alarm(2);

try {
$this->expectException(JsonCouldNotBeEncoded::class);

map($parent)->toJson();
} finally {
pcntl_alarm(0);
pcntl_signal(SIGALRM, SIG_DFL);
}
}

#[Test]
public function nested_objects_do_not_resolve_unused_mappers(): void
{
map(new ObjectA('a', 'b'))->toArray();

$this->container
->get(MapperConfig::class)
->addMapper(MapperResolutionProbe::class);

MapperResolutionProbe::$constructions = 0;

map(new ObjectWithMapperResolutionChildren([
new MapperResolutionChild('a'),
new MapperResolutionChild('b'),
new MapperResolutionChild('c'),
]))->toArray();

$this->assertSame(0, MapperResolutionProbe::$constructions);
}
}

final readonly class ObjectWithMapperResolutionChildren
{
public function __construct(
/** @var \Tests\Tempest\Integration\Mapper\Mappers\MapperResolutionChild[] */
public array $children,
) {}
}

final readonly class MapperResolutionChild
{
public function __construct(
public string $name,
) {}
}

final class MapperResolutionProbe implements Mapper
{
public static int $constructions = 0;

public function __construct()
{
self::$constructions++;
}

public function canMap(mixed $from, mixed $to): bool
{
return false;
}

public function map(mixed $from, mixed $to): mixed
{
return $from;
}
}
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
namespace Tests\Tempest\Integration\Mapper\Mappers;

use PHPUnit\Framework\Attributes\Test;
use Tempest\DateTime\DateTime;
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
use Tests\Tempest\Integration\Mapper\Fixtures\NestedObjectWithDate;
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectA;
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithNestedObjectAndDate;
use Tests\Tempest\Integration\Mapper\Fixtures\ObjectWithScalarValues;

use function Tempest\Mapper\map;
Expand Down Expand Up @@ -42,4 +45,29 @@ public function object_with_scalar_values_to_json(): void
json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR),
);
}

#[Test]
public function object_with_single_nested_object_to_json(): void
{
$date = DateTime::parse('2026-08-19T12:34:56+00:00');

$json = map(new ObjectWithNestedObjectAndDate(
createdAt: $date,
child: new NestedObjectWithDate($date),
children: [new NestedObjectWithDate($date)],
))->toJson();

$this->assertSame(
[
'createdAt' => '2026-08-19T12:34:56.000Z',
'child' => [
'createdAt' => '2026-08-19T12:34:56.000Z',
],
'children' => [
['createdAt' => '2026-08-19T12:34:56.000Z'],
],
],
json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR),
);
}
}
Loading
Loading

Back | FazBrowse Home | New Git URL