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: Supports dynamic properties (including stdClass) on the target side. #19

Merged
merged 1 commit into from
Feb 20, 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
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);
}
}
Loading