Skip to content

Commit

Permalink
Merge pull request #27 from rekalogika:feat/target-dynamic-properties
Browse files Browse the repository at this point in the history
feat: Dynamic properties (`stdClass` & co) on the target side.
  • Loading branch information
priyadi committed Feb 20, 2024
2 parents cd18fc7 + 7cd04fe commit 1a07a0e
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 18 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@

* feat: `PresetTransformer`.
* fix: Typo in `RemoveOptionalDefinitionPass`
* feat: Supports dynamic properties (including `stdClass`) on the target side.
* feat: Supports dynamic properties (including `stdClass`) on the source side.
* fix(`Mapper`): Fix typehint.
* test: test array cast to object mapping
* feat(`Context`): `with()` not accepts multiple argument.
* build: Deinternalize `ObjectCacheFactory`
* fix(`PresetMapping`): Support proxied classes, add tests.
* fix: Disallow proxy for objects with dynamic properties, including `stdClass`.
* feat: Dynamic properties (`stdClass` & co) on the target side.

## 1.0.0

Expand Down
7 changes: 4 additions & 3 deletions src/Transformer/Implementation/ObjectToObjectTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,10 @@ private function readSourcePropertyAndWriteTargetProperty(
$targetWriteVisibility = $propertyMapping->getTargetWriteVisibility();

if (
$targetWriteMode !== WriteMode::Method
&& $targetWriteMode !== WriteMode::Property
&& $targetWriteMode !== WriteMode::AdderRemover
in_array($targetWriteMode, [
WriteMode::None,
WriteMode::Constructor,
], true)
) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public function createObjectToObjectMetadata(
throw new InternalClassUnsupportedException($sourceClass);
}

if ($targetReflection->isInternal()) {
if (!$targetAllowsDynamicProperties && $targetReflection->isInternal()) {
throw new InternalClassUnsupportedException($targetClass);
}

Expand Down Expand Up @@ -126,7 +126,18 @@ public function createObjectToObjectMetadata(

$propertyMappings = [];

foreach ($targetProperties as $targetProperty) {
// determine properties to map

if ($targetAllowsDynamicProperties) {
$sourceProperties = $this->listProperties($sourceClass);
$propertiesToMap = array_unique(array_merge($sourceProperties, $targetProperties));
} else {
$propertiesToMap = $targetProperties;
}

// iterate over properties to map

foreach ($propertiesToMap as $targetProperty) {
$sourceProperty = $targetProperty;

// determine if a property mapper is defined for the property
Expand Down Expand Up @@ -176,9 +187,16 @@ public function createObjectToObjectMetadata(
// process target read mode

if ($targetReadInfo === null) {
$targetReadMode = ReadMode::None;
$targetReadName = null;
$targetReadVisibility = Visibility::None;
// if source allows dynamic properties, including stdClass
if ($targetAllowsDynamicProperties) {
$targetReadMode = ReadMode::DynamicProperty;
$targetReadName = $targetProperty;
$targetReadVisibility = Visibility::Public;
} else {
$targetReadMode = ReadMode::None;
$targetReadName = null;
$targetReadVisibility = Visibility::None;
}
} else {
$targetReadMode = match ($targetReadInfo->getType()) {
PropertyReadInfo::TYPE_METHOD => ReadMode::Method,
Expand Down Expand Up @@ -223,15 +241,22 @@ public function createObjectToObjectMetadata(
};

if ($targetWriteMode === WriteMode::None) {
continue;
if (!$targetAllowsDynamicProperties) {
continue;
}
$targetWriteMode = WriteMode::DynamicProperty;
$targetWriteName = $targetProperty;
$targetWriteVisibility = Visibility::Public;

} else {
$targetWriteName = $targetWriteInfo->getName();
$targetWriteVisibility = match ($targetWriteInfo->getVisibility()) {
PropertyWriteInfo::VISIBILITY_PUBLIC => Visibility::Public,
PropertyWriteInfo::VISIBILITY_PROTECTED => Visibility::Protected,
PropertyWriteInfo::VISIBILITY_PRIVATE => Visibility::Private,
default => Visibility::None,
};
}
$targetWriteName = $targetWriteInfo->getName();
$targetWriteVisibility = match ($targetWriteInfo->getVisibility()) {
PropertyWriteInfo::VISIBILITY_PUBLIC => Visibility::Public,
PropertyWriteInfo::VISIBILITY_PROTECTED => Visibility::Protected,
PropertyWriteInfo::VISIBILITY_PRIVATE => Visibility::Private,
default => Visibility::None,
};
}

// get source property types
Expand Down
1 change: 1 addition & 0 deletions src/Transformer/ObjectToObjectMetadata/WriteMode.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ enum WriteMode
case Property;
case AdderRemover;
case Constructor;
case DynamicProperty;
}
12 changes: 11 additions & 1 deletion src/Transformer/Util/ReaderWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ public function readSourceProperty(
if (isset($source->{$accessorName})) {
return $source->{$accessorName};
}
return null;

return null;
}

return null;
} catch (\Error $e) {
$message = $e->getMessage();
Expand Down Expand Up @@ -119,7 +120,14 @@ public function readTargetProperty(
} elseif ($readMode === ReadMode::Method) {
/** @psalm-suppress MixedMethodCall */
return $target->{$accessorName}();
} elseif ($readMode === ReadMode::DynamicProperty) {
if (isset($target->{$accessorName})) {
return $target->{$accessorName};
}

return null;
}

return null;
} catch (\Error $e) {
$message = $e->getMessage();
Expand Down Expand Up @@ -164,6 +172,8 @@ public function writeTargetProperty(
$target->{$accessorName}($value);
} elseif ($writeMode === WriteMode::AdderRemover) {
// noop
} elseif ($writeMode === WriteMode::DynamicProperty) {
$target->{$accessorName} = $value;
}
} catch (\Throwable $e) {
throw new UnableToWriteException(
Expand Down
54 changes: 54 additions & 0 deletions tests/IntegrationTest/DynamicPropertyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@

use Rekalogika\Mapper\Tests\Common\FrameworkTestCase;
use Rekalogika\Mapper\Tests\Fixtures\DynamicProperty\ObjectExtendingStdClass;
use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties;
use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithScalarPropertiesDto;

class DynamicPropertyTest extends FrameworkTestCase
{
// from stdclass to object

public function testStdClassToObject(): void
{
$source = new \stdClass();
Expand Down Expand Up @@ -86,4 +89,55 @@ public function testStdClassWithoutPropertiesToObject(): void
$this->assertNull($target->c);
$this->assertNull($target->d);
}

// to stdClass

public function testObjectToStdClass(): void
{
$source = new ObjectWithScalarProperties();
$target = $this->mapper->map($source, \stdClass::class);

$this->assertInstanceOf(\stdClass::class, $target);

$this->assertSame(1, $target->a);
$this->assertSame('string', $target->b);
$this->assertTrue($target->c);
$this->assertSame(1.1, $target->d);
}

public function testObjectToObjectExtendingStdClass(): void
{
$source = new ObjectWithScalarProperties();
$target = $this->mapper->map($source, ObjectExtendingStdClass::class);

$this->assertInstanceOf(ObjectExtendingStdClass::class, $target);

/** @psalm-suppress UndefinedPropertyFetch */
$this->assertSame(1, $target->a);
/** @psalm-suppress UndefinedPropertyFetch */
$this->assertSame('string', $target->b);
/** @psalm-suppress UndefinedPropertyFetch */
$this->assertTrue($target->c);
/** @psalm-suppress UndefinedPropertyFetch */
$this->assertSame(1.1, $target->d);
}

// stdclass to stdclass

public function testStdClassToStdClass(): void
{
$source = new \stdClass();
$source->a = 1;
$source->b = 'string';
$source->c = true;
$source->d = 1.1;

$target = $this->mapper->map($source, \stdClass::class);

$this->assertInstanceOf(\stdClass::class, $target);
$this->assertSame(1, $target->a);
$this->assertSame('string', $target->b);
$this->assertTrue($target->c);
$this->assertSame(1.1, $target->d);
}
}

0 comments on commit 1a07a0e

Please sign in to comment.