Skip to content
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
19 changes: 16 additions & 3 deletions castor.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,22 @@ function debug_mapper(string $source, string $target, string $load = '')
{
require_once __DIR__ . '/vendor/autoload.php';

if ($load) {
require_once $load;
}
// special autoloader for "AutoMapperTests"
spl_autoload_register(function (string $class) {
if (!str_starts_with($class, 'AutoMapper\\Tests\\AutoMapperTest\\')) {
return false;
}

// split on namespace separator
$parts = explode('\\', $class);
// get second part
$testDirectory = $parts[3] ?? '';
$mapFile = __DIR__ . '/tests/AutoMapperTest/' . $testDirectory . '/map.php';

if (file_exists($mapFile)) {
require_once $mapFile;
}
});

$automapper = AutoMapper\AutoMapper::create();
// get private property loader value
Expand Down
18 changes: 10 additions & 8 deletions src/Attribute/MapFrom.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
final readonly class MapFrom
{
/**
* @param class-string<object>|'array'|array<class-string<object>|'array'>|null $source The specific source class name or array. If null this attribute will be used for all source classes.
* @param string|null $property The source property name. If null, the target property name will be used.
* @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used.
* @param string|callable(mixed $value, object|array<string, mixed> $source, array<string, mixed> $context): mixed $transformer A transformer id or a callable that transform the value during mapping
* @param bool|null $ignore If true, the property will be ignored during mapping
* @param string|null $if The condition to map the property, using the expression language
* @param string[]|null $groups The groups to map the property
* @param string|null $dateTimeFormat The date-time format to use when transforming this property
* @param class-string<object>|'array'|array<class-string<object>|'array'>|null $source The specific source class name or array. If null this attribute will be used for all source classes.
* @param string|null $property The source property name. If null, the target property name will be used.
* @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used.
* @param string|callable(mixed $value, object|array<string, mixed> $source, array<string, mixed> $context): mixed $transformer A transformer id or a callable that transform the value during mapping
* @param bool|null $ignore If true, the property will be ignored during mapping
* @param string|null $if The condition to map the property, using the expression language
* @param string[]|null $groups The groups to map the property
* @param string|null $dateTimeFormat The date-time format to use when transforming this property
* @param bool|null $extractTypesFromGetter If true, the types will be extracted from the getter method
*/
public function __construct(
public string|array|null $source = null,
Expand All @@ -30,6 +31,7 @@ public function __construct(
public ?array $groups = null,
public int $priority = 0,
public ?string $dateTimeFormat = null,
public ?bool $extractTypesFromGetter = null,
) {
}
}
18 changes: 10 additions & 8 deletions src/Attribute/MapTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
final readonly class MapTo
{
/**
* @param class-string<object>|'array'|array<class-string<object>|'array'>|null $target The specific target class name or array. If null this attribute will be used for all target classes.
* @param string|null $property The target property name. If null, the source property name will be used.
* @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used.
* @param string|callable(mixed $value, object|array<string, mixed> $source, array<string, mixed> $context): mixed $transformer A transformer id or a callable that transform the value during mapping
* @param bool|null $ignore If true, the property will be ignored during mapping
* @param string|null $if The condition to map the property, using the expression language
* @param string[]|null $groups The groups to map the property
* @param string|null $dateTimeFormat The date-time format to use when transforming this property
* @param class-string<object>|'array'|array<class-string<object>|'array'>|null $target The specific target class name or array. If null this attribute will be used for all target classes.
* @param string|null $property The target property name. If null, the source property name will be used.
* @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used.
* @param string|callable(mixed $value, object|array<string, mixed> $source, array<string, mixed> $context): mixed $transformer A transformer id or a callable that transform the value during mapping
* @param bool|null $ignore If true, the property will be ignored during mapping
* @param string|null $if The condition to map the property, using the expression language
* @param string[]|null $groups The groups to map the property
* @param string|null $dateTimeFormat The date-time format to use when transforming this property
* @param bool|null $extractTypesFromGetter If true, the types will be extracted from the getter method
*/
public function __construct(
public string|array|null $target = null,
Expand All @@ -30,6 +31,7 @@ public function __construct(
public ?array $groups = null,
public int $priority = 0,
public ?string $dateTimeFormat = null,
public ?bool $extractTypesFromGetter = null,
) {
}
}
8 changes: 8 additions & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ public function __construct(
* Allow extra properties to be mapped when the target or the source implements ArrayAccess class.
*/
public bool $allowExtraProperties = false,
/**
* When extracting types from a target property we generally try to use the one from where we write.
*
* However, this may cause bad type in case of covariance where the type should be extracted from the setter.
*
* Enable this option to extract the type from the getter instead of the setter.
*/
public bool $extractTypesFromGetter = false,
) {
}
}
1 change: 1 addition & 0 deletions src/Event/PropertyMetadataEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public function __construct(
public ?bool $disableGroupsCheck = null,
public int $priority = 0,
public readonly bool $isFromDefaultExtractor = false,
public ?bool $extractTypesFromGetter = null,
) {
}
}
7 changes: 4 additions & 3 deletions src/EventListener/MapFromListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ private function addPropertyFromTarget(GenerateMapperEvent $event, MapFrom $mapF
$sourceProperty = new SourcePropertyMetadata($mapFrom->property ?? $property);
$targetProperty = new TargetPropertyMetadata($property);

$property = new PropertyMetadataEvent(
$propertyMetadata = new PropertyMetadataEvent(
mapperMetadata: $event->mapperMetadata,
source: $sourceProperty,
target: $targetProperty,
Expand All @@ -83,12 +83,13 @@ private function addPropertyFromTarget(GenerateMapperEvent $event, MapFrom $mapF
if: $mapFrom->if,
groups: $mapFrom->groups,
priority: $mapFrom->priority,
extractTypesFromGetter: $mapFrom->extractTypesFromGetter,
);

if (\array_key_exists($property->target->property, $event->properties) && $event->properties[$property->target->property]->priority >= $property->priority) {
if (\array_key_exists($propertyMetadata->target->property, $event->properties) && $event->properties[$propertyMetadata->target->property]->priority >= $propertyMetadata->priority) {
return;
}

$event->properties[$property->target->property] = $property;
$event->properties[$propertyMetadata->target->property] = $propertyMetadata;
}
}
7 changes: 4 additions & 3 deletions src/EventListener/MapToListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ private function addPropertyFromSource(GenerateMapperEvent $event, MapTo $mapTo,
$sourceProperty = new SourcePropertyMetadata($property);
$targetProperty = new TargetPropertyMetadata($mapTo->property ?? $property);

$property = new PropertyMetadataEvent(
$propertyMetadata = new PropertyMetadataEvent(
mapperMetadata: $event->mapperMetadata,
source: $sourceProperty,
target: $targetProperty,
Expand All @@ -84,12 +84,13 @@ private function addPropertyFromSource(GenerateMapperEvent $event, MapTo $mapTo,
if: $mapTo->if,
groups: $mapTo->groups,
priority: $mapTo->priority,
extractTypesFromGetter: $mapTo->extractTypesFromGetter,
);

if (\array_key_exists($property->target->property, $event->properties) && $event->properties[$property->target->property]->priority >= $property->priority) {
if (\array_key_exists($propertyMetadata->target->property, $event->properties) && $event->properties[$propertyMetadata->target->property]->priority >= $propertyMetadata->priority) {
return;
}

$event->properties[$property->target->property] = $property;
$event->properties[$propertyMetadata->target->property] = $propertyMetadata;
}
}
2 changes: 1 addition & 1 deletion src/Extractor/FromSourceMappingExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*/
final class FromSourceMappingExtractor extends MappingExtractor
{
public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty): TypesMatching
public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty, bool $extractTypesFromGetter): TypesMatching
{
$types = new TypesMatching();
$sourceTypes = $this->propertyInfoExtractor->getTypes($source, $sourceProperty->property, [
Expand Down
3 changes: 2 additions & 1 deletion src/Extractor/FromTargetMappingExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
*/
final class FromTargetMappingExtractor extends MappingExtractor
{
public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty): TypesMatching
public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty, bool $extractTypesFromGetter): TypesMatching
{
$types = new TypesMatching();
$targetTypes = $this->propertyInfoExtractor->getTypes($target, $targetProperty->property, [
ReadWriteTypeExtractor::WRITE_MUTATOR => $targetProperty->writeMutator,
ReadWriteTypeExtractor::EXTRACT_TYPE_FROM_GETTER => $extractTypesFromGetter,
]) ?? [];

foreach ($targetTypes as $type) {
Expand Down
2 changes: 1 addition & 1 deletion src/Extractor/MappingExtractorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ interface MappingExtractorInterface
*/
public function getProperties(string $class): iterable;

public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty): TypesMatching;
public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty, bool $extractTypesFromGetter): TypesMatching;

public function getDateTimeFormat(PropertyMetadataEvent $propertyMetadataEvent): string;

Expand Down
3 changes: 2 additions & 1 deletion src/Extractor/ReadWriteTypeExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
{
public const READ_ACCESSOR = 'read_accessor';
public const WRITE_MUTATOR = 'write_mutator';
public const EXTRACT_TYPE_FROM_GETTER = 'extract_type_from_getter';

/**
* @param array<string, mixed> $context
Expand All @@ -28,7 +29,7 @@ public function getTypes(string $class, string $property, array $context = []):
return $accessor->getTypes($class);
}

if (($mutator = $context[self::WRITE_MUTATOR] ?? false) && $mutator instanceof WriteMutator) {
if (!($context[self::EXTRACT_TYPE_FROM_GETTER] ?? false) && ($mutator = $context[self::WRITE_MUTATOR] ?? false) && $mutator instanceof WriteMutator) {
return $mutator->getTypes($class);
}

Expand Down
4 changes: 2 additions & 2 deletions src/Extractor/SourceTargetMappingExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
*/
class SourceTargetMappingExtractor extends MappingExtractor
{
public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty): TypesMatching
public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty, bool $extractTypesFromGetter): TypesMatching
{
$sourceTypes = $this->propertyInfoExtractor->getTypes($source, $sourceProperty->property, [ReadWriteTypeExtractor::READ_ACCESSOR => $sourceProperty->accessor]) ?? [];
$targetTypes = $this->propertyInfoExtractor->getTypes($target, $targetProperty->property, [ReadWriteTypeExtractor::WRITE_MUTATOR => $targetProperty->writeMutator]) ?? [];
$targetTypes = $this->propertyInfoExtractor->getTypes($target, $targetProperty->property, [ReadWriteTypeExtractor::WRITE_MUTATOR => $targetProperty->writeMutator, ReadWriteTypeExtractor::EXTRACT_TYPE_FROM_GETTER => $extractTypesFromGetter]) ?? [];

return TypesMatching::fromSourceAndTargetTypes($sourceTypes, $targetTypes);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Metadata/MetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
$targetPropertyMetadata = TargetPropertyMetadata::fromEvent($propertyMappedEvent->target);

if (null === $propertyMappedEvent->types) {
$propertyMappedEvent->types = $extractor->getTypes($mapperMetadata->source, $sourcePropertyMetadata, $mapperMetadata->target, $targetPropertyMetadata);
$propertyMappedEvent->types = $extractor->getTypes($mapperMetadata->source, $sourcePropertyMetadata, $mapperMetadata->target, $targetPropertyMetadata, $propertyMappedEvent->extractTypesFromGetter ?? $this->configuration->extractTypesFromGetter);
}

if (null === $propertyMappedEvent->transformer) {
Expand Down
3 changes: 3 additions & 0 deletions tests/AutoMapperTest/Covariance/expected.data
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
AutoMapper\Tests\AutoMapperTest\Covariance\ExtendedA {
#b: AutoMapper\Tests\AutoMapperTest\Covariance\ExtendedB {}
}
60 changes: 60 additions & 0 deletions tests/AutoMapperTest/Covariance/map.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\AutoMapperTest\Covariance;

use AutoMapper\Attribute\MapTo;
use AutoMapper\Tests\AutoMapperBuilder;

class GenericB
{
}

class GenericA
{
/**
* @var GenericB
*/
#[MapTo(extractTypesFromGetter: true)]
protected $b;

public function getB(): GenericB
{
return $this->b;
}

public function setB(GenericB $b): void
{
$this->b = $b;
}
}

class ExtendedB extends GenericB
{
public function specificToB(): string
{
return 'result from ExtendedB';
}
}

class ExtendedA extends GenericA
{
/**
* @var ExtendedB
*/
protected $b;

public function getB(): ExtendedB
{
return $this->b;
}
}

$autoMapper = AutoMapperBuilder::buildAutoMapper(mapPrivatePropertiesAndMethod: true);

$genericA = new GenericA();
$genericB = new GenericB();
$genericA->setB($genericB);

return $autoMapper->map($genericA, ExtendedA::class);