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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Initial support for nested properties
- Add support for object invokable transformer in attribute transformer
- Add a new interface `PropertyTransformerComputeInterface` to allow property transformers with supports, to compute a value that will be fixed during code generation.
- Support ObjectMapper attributes
- Add an implementation for Symfony `ObjectMapperInterface` using AutoMapper

### Changed
- [BC Break] `PropertyTransformerSupportInterface` does not use a `TypesMatching` anymore, you can get the type directly from `SourcePropertyMetadata` or `TargetPropertyMetadata`.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"symfony/framework-bundle": "^7.4 || ^8.0",
"symfony/http-client": "^7.4 || ^8.0",
"symfony/http-kernel": "^7.4 || ^8.0",
"symfony/object-mapper": "^7.4 || ^8.0",
"symfony/phpunit-bridge": "^8.0",
"symfony/serializer": "^7.4 || ^8.0",
"symfony/stopwatch": "^7.4 || ^8.0",
Expand Down
6 changes: 6 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ parameters:
- src/

tmpDir: cache

ignoreErrors:
-
message: "#^Method AutoMapper\\\\ObjectMapper\\\\ObjectMapper\\:\\:map\\(\\) should return T of object but returns object\\|null\\.$#"
count: 1
path: src/ObjectMapper/ObjectMapper.php
41 changes: 41 additions & 0 deletions src/AttributeReference/Reference.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

namespace AutoMapper\AttributeReference;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;

class Reference
{
public function __construct(
Expand All @@ -14,4 +19,40 @@ public function __construct(
public ?string $methodName = null,
) {
}

public function getReferenceExpression(): Expr
{
if ($this->methodName) {
/** ReflectionReference::fromMethod($className, $methodName) */
return new Expr\StaticCall(
new Name\FullyQualified(ReflectionReference::class),
'fromMethod',
[
new Arg(new Scalar\String_($this->className)),
new Arg(new Scalar\String_($this->methodName)),
]
);
}

if ($this->propertyName) {
/** ReflectionReference::fromProperty($className, $propertyName) */
return new Expr\StaticCall(
new Name\FullyQualified(ReflectionReference::class),
'fromProperty',
[
new Arg(new Scalar\String_($this->className)),
new Arg(new Scalar\String_($this->propertyName)),
]
);
}

/** ReflectionReference::fromClass($className) */
return new Expr\StaticCall(
new Name\FullyQualified(ReflectionReference::class),
'fromClass',
[
new Arg(new Scalar\String_($this->className)),
]
);
}
}
10 changes: 10 additions & 0 deletions src/AutoMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ public function mapCollection(iterable $collection, string $target, array $conte
/**
* @param ProviderInterface[] $providers
* @param iterable<string|int, PropertyTransformerInterface> $propertyTransformers
* @param iterable<string|int, object> $extraMapperServices
*
* @return self
*/
Expand All @@ -142,6 +143,7 @@ public static function create(
EventDispatcherInterface $eventDispatcher = new EventDispatcher(),
iterable $providers = [],
?ObjectManager $objectManager = null,
iterable $extraMapperServices = [],
): AutoMapperInterface {
if (class_exists(AttributeLoader::class)) {
$loaderClass = new AttributeLoader();
Expand Down Expand Up @@ -190,6 +192,14 @@ public static function create(
}
}

foreach ($extraMapperServices as $key => $mapperService) {
if (\is_int($key)) {
$key = $mapperService::class;
}

$serviceLocator->set($key, $mapperService);
}

$metadataRegistry = new MetadataRegistry($configuration);
$classDiscriminatorResolver = new ClassDiscriminatorResolver();

Expand Down
6 changes: 4 additions & 2 deletions src/Event/GenerateMapperEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@
use AutoMapper\ConstructorStrategy;
use AutoMapper\Metadata\Discriminator;
use AutoMapper\Metadata\MapperMetadata;
use AutoMapper\Metadata\Provider;
use Symfony\Contracts\EventDispatcher\Event;

/**
* @internal
*/
final class GenerateMapperEvent
final class GenerateMapperEvent extends Event
{
/**
* @param PropertyMetadataEvent[] $properties A list of properties to add to this mapping
*/
public function __construct(
public readonly MapperMetadata $mapperMetadata,
public array $properties = [],
public ?string $provider = null,
public ?Provider $provider = null,
public ?bool $checkAttributes = null,
public ?ConstructorStrategy $constructorStrategy = null,
public ?bool $allowReadOnlyTargetToPopulate = null,
Expand Down
3 changes: 2 additions & 1 deletion src/Event/PropertyMetadataEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace AutoMapper\Event;

use AutoMapper\AttributeReference\Reference;
use AutoMapper\Metadata\MapperMetadata;
use AutoMapper\Transformer\TransformerInterface;

Expand All @@ -24,7 +25,7 @@ public function __construct(
public ?string $dateTimeFormat = null,
public ?bool $ignored = null,
public ?string $ignoreReason = null,
public ?string $if = null,
public string|Reference|null $if = null,
public ?array $groups = null,
public ?bool $disableGroupsCheck = null,
public int $priority = 0,
Expand Down
3 changes: 2 additions & 1 deletion src/EventListener/ApiPlatform/JsonLdListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use AutoMapper\Event\PropertyMetadataEvent;
use AutoMapper\Event\SourcePropertyMetadata;
use AutoMapper\Event\TargetPropertyMetadata;
use AutoMapper\Metadata\Provider;
use AutoMapper\Provider\ApiPlatform\IriProvider;
use AutoMapper\Transformer\ApiPlatform\JsonLdContextTransformer;
use AutoMapper\Transformer\ApiPlatform\JsonLdIdTransformer;
Expand Down Expand Up @@ -68,7 +69,7 @@ public function __invoke(GenerateMapperEvent $event): void
}

if ($event->mapperMetadata->source === 'array' && $this->resourceClassResolver->isResourceClass($event->mapperMetadata->target)) {
$event->provider ??= IriProvider::class;
$event->provider ??= new Provider(Provider::TYPE_SERVICE, IriProvider::class);
}
}
}
3 changes: 2 additions & 1 deletion src/EventListener/Doctrine/DoctrineProviderListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace AutoMapper\EventListener\Doctrine;

use AutoMapper\Event\GenerateMapperEvent;
use AutoMapper\Metadata\Provider;
use AutoMapper\Provider\Doctrine\DoctrineProvider;
use Doctrine\Persistence\ObjectManager;

Expand All @@ -21,6 +22,6 @@ public function __invoke(GenerateMapperEvent $event): void
return;
}

$event->provider ??= DoctrineProvider::class;
$event->provider ??= new Provider(Provider::TYPE_SERVICE, DoctrineProvider::class);
}
}
3 changes: 2 additions & 1 deletion src/EventListener/MapProviderListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use AutoMapper\Attribute\MapProvider;
use AutoMapper\Event\GenerateMapperEvent;
use AutoMapper\Exception\BadMapDefinitionException;
use AutoMapper\Metadata\Provider;

/**
* @internal
Expand Down Expand Up @@ -60,7 +61,7 @@ public function __invoke(GenerateMapperEvent $event): void
if (false === $eventProvider) {
$event->provider = null;
} else {
$event->provider = $eventProvider;
$event->provider = new Provider(Provider::TYPE_SERVICE, $eventProvider);
}
}
}
67 changes: 67 additions & 0 deletions src/EventListener/ObjectMapper/MapListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace AutoMapper\EventListener\ObjectMapper;

use AutoMapper\AttributeReference\Reference;
use AutoMapper\Exception\BadMapDefinitionException;
use AutoMapper\Transformer\CallableTransformer;
use AutoMapper\Transformer\ExpressionLanguageTransformer;
use AutoMapper\Transformer\ReferenceTransformer;
use AutoMapper\Transformer\ServiceLocatorTransformer;
use AutoMapper\Transformer\TransformerInterface;
use Psr\Container\ContainerInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\SyntaxError;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\TransformCallableInterface;

abstract readonly class MapListener
{
public function __construct(
protected ContainerInterface $serviceLocator,
private ExpressionLanguage $expressionLanguage,
) {
}

protected function getTransformerFromMapAttribute(string $class, Map $attribute, Reference $reference, bool $fromSource = true): ?TransformerInterface
{
$transformer = null;
$transformerCallable = $attribute->transform;

if ($transformerCallable !== null) {
$callableName = null;

if ($transformerCallable instanceof \Closure) {
$transformer = new ReferenceTransformer($reference, true);
} elseif (!\is_object($transformerCallable) && \is_callable($transformerCallable, false, $callableName)) {
$transformer = new CallableTransformer($callableName);
} elseif (\is_callable($transformerCallable, false, $callableName)) {
$transformer = new ReferenceTransformer($reference, true);
} elseif (\is_string($transformerCallable) && method_exists($class, $transformerCallable)) {
$reflMethod = new \ReflectionMethod($class, $transformerCallable);

if ($reflMethod->isStatic()) {
$transformer = new CallableTransformer($class . '::' . $transformerCallable);
} else {
$transformer = new CallableTransformer($transformerCallable, $fromSource, !$fromSource);
}
} elseif (\is_string($transformerCallable) && class_exists($transformerCallable) && is_subclass_of($transformerCallable, TransformCallableInterface::class)) {
$transformer = new ServiceLocatorTransformer($transformerCallable);
} elseif (\is_string($transformerCallable)) {
try {
$expression = $this->expressionLanguage->compile($transformerCallable, ['value' => 'source', 'context']);
} catch (SyntaxError $e) {
throw new BadMapDefinitionException(\sprintf('Transformer "%s" targeted by %s transformer on class "%s" is not valid.', $transformerCallable, $attribute::class, $class), 0, $e);
}

$transformer = new ExpressionLanguageTransformer($expression);
} else {
throw new BadMapDefinitionException(\sprintf('Callable "%s" targeted by %s transformer on class "%s" is not valid.', json_encode($transformerCallable), $attribute::class, $class));
}
}

return $transformer;
}
}
105 changes: 105 additions & 0 deletions src/EventListener/ObjectMapper/MapSourceListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

declare(strict_types=1);

namespace AutoMapper\EventListener\ObjectMapper;

use AutoMapper\AttributeReference\Reference;
use AutoMapper\Event\GenerateMapperEvent;
use AutoMapper\Event\PropertyMetadataEvent;
use AutoMapper\Event\SourcePropertyMetadata;
use AutoMapper\Event\TargetPropertyMetadata;
use AutoMapper\Metadata\Provider;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\Condition\TargetClass;

final readonly class MapSourceListener extends MapListener
{
public function __invoke(GenerateMapperEvent $event): void
{
// only handle class to class mapping
if (!$event->mapperMetadata->sourceReflectionClass) {
return;
}

$mapAttribute = null;
$hasAnyMapAttribute = false;

foreach ($event->mapperMetadata->sourceReflectionClass->getAttributes(Map::class) as $sourceAttribute) {
/** @var Map $attribute */
$attribute = $sourceAttribute->newInstance();
$hasAnyMapAttribute = true;

if (!$attribute->target || $attribute->target === $event->mapperMetadata->target) {
$mapAttribute = $attribute;
break;
}
}

// it means that there is at least one Map attribute but none match the current mapping
if (!$mapAttribute && $hasAnyMapAttribute) {
return;
}

// get all properties
$properties = [];

foreach ($event->mapperMetadata->sourceReflectionClass->getProperties() as $property) {
foreach ($property->getAttributes(Map::class) as $index => $propertyAttribute) {
/** @var Map $attribute */
$attribute = $propertyAttribute->newInstance();
$reference = new Reference(Map::class, $index, $event->mapperMetadata->sourceReflectionClass->getName(), propertyName: $property->getName());
$propertyMetadata = new PropertyMetadataEvent(
/*
* public ?string $if = null,// @TODO
*/
$event->mapperMetadata,
new SourcePropertyMetadata($property->getName()),
new TargetPropertyMetadata($attribute->target ?? $property->getName()),
transformer: $this->getTransformerFromMapAttribute($event->mapperMetadata->sourceReflectionClass->getName(), $attribute, $reference, true),
);

$ifCallableName = null;

if ($attribute->if instanceof TargetClass) {
$reflectionObject = new \ReflectionClass($attribute->if);
/** @var string $targetClassName */
$targetClassName = $reflectionObject->getProperty('className')->getRawValue($attribute->if);

if ($targetClassName !== null && $event->mapperMetadata->target !== $targetClassName && !is_subclass_of($event->mapperMetadata->target, $targetClassName)) {
continue;
}
} elseif ($attribute->if && \is_callable($attribute->if, false, $ifCallableName)) {
if (\is_object($attribute->if)) {
$propertyMetadata->if = $reference;
} else {
$propertyMetadata->if = $ifCallableName;
}
} elseif (\is_string($attribute->if)) {
$propertyMetadata->if = $attribute->if;
}

$properties[] = $propertyMetadata;
}
}

$event->properties = [...$event->properties, ...$properties];

if ($mapAttribute?->transform) {
$callableName = null;

if (\is_callable($mapAttribute->transform, false, $callableName)) {
$event->provider = new Provider(Provider::TYPE_CALLABLE, $callableName, true);
}

if (\is_string($mapAttribute->transform) && $this->serviceLocator->has($mapAttribute->transform)) {
$event->provider = new Provider(Provider::TYPE_SERVICE_CALLABLE, $mapAttribute->transform, true);
}
}

// Stop propagation if any Map attribute is found
if ($hasAnyMapAttribute || \count($properties) > 0 || $mapAttribute) {
$event->stopPropagation();
}
}
}
Loading