Skip to content

Commit

Permalink
Merge pull request #19 from rekalogika:feat/source-dynamic-properties
Browse files Browse the repository at this point in the history
feat: Supports dynamic properties (including `stdClass`) on the target side.
  • Loading branch information
priyadi committed Feb 20, 2024
2 parents d1993e9 + d40e296 commit eeacf75
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* feat: `PresetTransformer`.
* fix: Typo in `RemoveOptionalDefinitionPass`
* feat: Supports dynamic properties (including `stdClass`) on the target side.

## 1.0.0

Expand Down
3 changes: 2 additions & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
failOnRisky="true"
failOnWarning="false"
cacheDirectory=".phpunit.cache"
beStrictAboutCoverageMetadata="true">
beStrictAboutCoverageMetadata="true"
displayDetailsOnTestsThatTriggerWarnings="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,12 @@ public function createObjectToObjectMetadata(

$targetReflection = new \ReflectionClass($targetClass);

// check if source and target classes are internal
$sourceAllowsDynamicProperties = $this->allowsDynamicProperties($sourceReflection);
$targetAllowsDynamicProperties = $this->allowsDynamicProperties($targetReflection);

if ($sourceReflection->isInternal()) {
// check if source and target classes are internal. we allow stdClass at
// the source side
if (!$sourceAllowsDynamicProperties && $sourceReflection->isInternal()) {
throw new InternalClassUnsupportedException($sourceClass);
}

Expand Down Expand Up @@ -143,9 +146,16 @@ public function createObjectToObjectMetadata(
// process source read mode

if ($sourceReadInfo === null) {
$sourceReadMode = ReadMode::None;
$sourceReadName = null;
$sourceReadVisibility = Visibility::None;
// if source allows dynamic properties, including stdClass
if ($sourceAllowsDynamicProperties) {
$sourceReadMode = ReadMode::DynamicProperty;
$sourceReadName = $sourceProperty;
$sourceReadVisibility = Visibility::Public;
} else {
$sourceReadMode = ReadMode::None;
$sourceReadName = null;
$sourceReadVisibility = Visibility::None;
}
} else {
$sourceReadMode = match ($sourceReadInfo->getType()) {
PropertyReadInfo::TYPE_METHOD => ReadMode::Method,
Expand Down Expand Up @@ -320,6 +330,8 @@ public function createObjectToObjectMetadata(
sourceClass: $sourceClass,
targetClass: $targetClass,
providedTargetClass: $providedTargetClass,
sourceAllowsDynamicProperties: $sourceAllowsDynamicProperties,
targetAllowsDynamicProperties: $targetAllowsDynamicProperties,
allPropertyMappings: $propertyMappings,
instantiable: $instantiable,
cloneable: $cloneable,
Expand Down Expand Up @@ -412,4 +424,18 @@ private function listInitializableProperties(

return $initializableProperties;
}

/**
* @param \ReflectionClass<object> $class
*/
private function allowsDynamicProperties(\ReflectionClass $class): bool
{
do {
if (count($class->getAttributes(\AllowDynamicProperties::class)) > 0) {
return true;
}
} while ($class = $class->getParentClass());

return false;
}
}
64 changes: 40 additions & 24 deletions src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public function __construct(
private string $sourceClass,
private string $targetClass,
private string $providedTargetClass,
private bool $sourceAllowsDynamicProperties,
private bool $targetAllowsDynamicProperties,
array $allPropertyMappings,
private bool $instantiable,
private bool $cloneable,
Expand Down Expand Up @@ -104,18 +106,20 @@ public function withTargetProxy(
bool $constructorIsEager,
): self {
return new self(
$this->sourceClass,
$this->targetClass,
$this->providedTargetClass,
$this->allPropertyMappings,
$this->instantiable,
$this->cloneable,
$this->initializableTargetPropertiesNotInSource,
$this->sourceModifiedTime,
$this->targetModifiedTime,
$this->targetReadOnly,
$constructorIsEager,
$targetProxySkippedProperties,
sourceClass: $this->sourceClass,
targetClass: $this->targetClass,
providedTargetClass: $this->providedTargetClass,
sourceAllowsDynamicProperties: $this->sourceAllowsDynamicProperties,
targetAllowsDynamicProperties: $this->targetAllowsDynamicProperties,
allPropertyMappings: $this->allPropertyMappings,
instantiable: $this->instantiable,
cloneable: $this->cloneable,
initializableTargetPropertiesNotInSource: $this->initializableTargetPropertiesNotInSource,
sourceModifiedTime: $this->sourceModifiedTime,
targetModifiedTime: $this->targetModifiedTime,
targetReadOnly: $this->targetReadOnly,
constructorIsEager: $constructorIsEager,
targetProxySkippedProperties: $targetProxySkippedProperties,
cannotUseProxyReason: null
);
}
Expand All @@ -124,18 +128,20 @@ public function withReasonCannotUseProxy(
string $reason
): self {
return new self(
$this->sourceClass,
$this->targetClass,
$this->providedTargetClass,
$this->allPropertyMappings,
$this->instantiable,
$this->cloneable,
$this->initializableTargetPropertiesNotInSource,
$this->sourceModifiedTime,
$this->targetModifiedTime,
$this->targetReadOnly,
$this->constructorIsEager,
[],
sourceClass: $this->sourceClass,
targetClass: $this->targetClass,
providedTargetClass: $this->providedTargetClass,
sourceAllowsDynamicProperties: $this->sourceAllowsDynamicProperties,
targetAllowsDynamicProperties: $this->targetAllowsDynamicProperties,
allPropertyMappings: $this->allPropertyMappings,
instantiable: $this->instantiable,
cloneable: $this->cloneable,
initializableTargetPropertiesNotInSource: $this->initializableTargetPropertiesNotInSource,
sourceModifiedTime: $this->sourceModifiedTime,
targetModifiedTime: $this->targetModifiedTime,
targetReadOnly: $this->targetReadOnly,
constructorIsEager: $this->constructorIsEager,
targetProxySkippedProperties: [],
cannotUseProxyReason: $reason,
);
}
Expand Down Expand Up @@ -277,4 +283,14 @@ public function constructorIsEager(): bool
{
return $this->constructorIsEager;
}

public function getSourceAllowsDynamicProperties(): bool
{
return $this->sourceAllowsDynamicProperties;
}

public function getTargetAllowsDynamicProperties(): bool
{
return $this->targetAllowsDynamicProperties;
}
}
1 change: 1 addition & 0 deletions src/Transformer/ObjectToObjectMetadata/ReadMode.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ enum ReadMode
case None;
case Method;
case Property;
case DynamicProperty;
}
6 changes: 6 additions & 0 deletions src/Transformer/Util/ReaderWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ public function readSourceProperty(
} elseif ($mode === ReadMode::Method) {
/** @psalm-suppress MixedMethodCall */
return $source->{$accessorName}();
} elseif ($mode === ReadMode::DynamicProperty) {
if (isset($source->{$accessorName})) {
return $source->{$accessorName};
} else {
return null;
}
}
return null;
} catch (\Error $e) {
Expand Down
4 changes: 4 additions & 0 deletions src/Util/ClassUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ public static function getLastModifiedTime(
$class = new \ReflectionClass($class);
}

if ($class->isInternal()) {
return 0;
}

$fileName = $class->getFileName();

if ($fileName === false) {
Expand Down
10 changes: 9 additions & 1 deletion templates/data_collector.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,12 @@
<tbody>
<tr>
<th>Source class</th>
<td>{{ metadata.sourceClass|abbr_class }}</td>
<td>
{{ metadata.sourceClass|abbr_class }}
{% if metadata.sourceAllowsDynamicProperties %}
<span class="label status-info">Allows dynamic properties</span>
{% endif %}
</td>
</tr>
<tr>
<th>Wanted target class</th>
Expand All @@ -267,6 +272,9 @@
{% if not metadata.instantiable %}
<span class="label status-error">Not instantiable</span>
{% endif %}
{% if metadata.targetAllowsDynamicProperties %}
<span class="label status-info">Allows dynamic properties</span>
{% endif %}
</td>
</tr>
<tr>
Expand Down
18 changes: 18 additions & 0 deletions tests/Fixtures/DynamicProperty/ObjectExtendingStdClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?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\DynamicProperty;

class ObjectExtendingStdClass extends \stdClass
{
}
71 changes: 71 additions & 0 deletions tests/IntegrationTest/DynamicPropertyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?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\DynamicProperty\ObjectExtendingStdClass;
use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithScalarPropertiesDto;

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

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

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

public function testObjectExtendingStdClassToObject(): void
{
$source = new ObjectExtendingStdClass();
/** @psalm-suppress UndefinedPropertyAssignment */
$source->a = 1;
/** @psalm-suppress UndefinedPropertyAssignment */
$source->b = 'string';
/** @psalm-suppress UndefinedPropertyAssignment */
$source->c = true;
/** @psalm-suppress UndefinedPropertyAssignment */
$source->d = 1.1;

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

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

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

$this->assertInstanceOf(ObjectWithScalarPropertiesDto::class, $target);
$this->assertNull($target->a);
$this->assertNull($target->b);
$this->assertNull($target->c);
$this->assertNull($target->d);
}
}

0 comments on commit eeacf75

Please sign in to comment.