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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add support for static callable in attribute transformer
- 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.

### Changed
- [BC Break] `PropertyTransformerSupportInterface` does not use a `TypesMatching` anymore, you can get the type directly from `SourcePropertyMetadata` or `TargetPropertyMetadata`.
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 @@ -16,6 +16,7 @@
use AutoMapper\Transformer\ApiPlatform\JsonLdIdTransformer;
use AutoMapper\Transformer\FixedValueTransformer;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformer;
use PhpParser\Node\Scalar;

final readonly class JsonLdListener
{
Expand Down Expand Up @@ -60,7 +61,7 @@ public function __invoke(GenerateMapperEvent $event): void
mapperMetadata: $event->mapperMetadata,
source: new SourcePropertyMetadata('@context'),
target: new TargetPropertyMetadata('@context'),
transformer: new PropertyTransformer(JsonLdContextTransformer::class, ['forced_resource_class' => $event->mapperMetadata->source]),
transformer: new PropertyTransformer(JsonLdContextTransformer::class, computedValueExpr: new Scalar\String_($event->mapperMetadata->source)),
if: "(context['normalizer_format'] ?? false) === 'jsonld' and (context['jsonld_has_context'] ?? false) === false and (context['depth'] ?? 0) <= 1",
disableGroupsCheck: true,
);
Expand Down
11 changes: 8 additions & 3 deletions src/Transformer/ApiPlatform/JsonLdContextTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,22 @@ public function __construct(
) {
}

public function transform(mixed $value, object|array $source, array $context): mixed
public function transform(mixed $value, object|array $source, array $context, mixed $computed = null): mixed
{
if (!\is_object($source)) {
return null;
}

$resourceClass = $context['forced_resource_class'] ?? $this->resourceClassResolver->isResourceClass($source::class) ? $this->resourceClassResolver->getResourceClass($source) : null;
$resourceClass = $computed ?? $this->resourceClassResolver->isResourceClass(
$source::class
) ? $this->resourceClassResolver->getResourceClass($source) : null;

if (null === $resourceClass) {
if ($this->contextBuilder instanceof AnonymousContextBuilderInterface) {
return $this->contextBuilder->getAnonymousResourceContext($source, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null]);
return $this->contextBuilder->getAnonymousResourceContext(
$source,
($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null]
);
}

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
/**
* @experimental
*/
interface PrioritizedPropertyTransformerInterface
interface PrioritizedPropertyTransformerInterface extends PropertyTransformerSupportInterface
{
public function getPriority(): int;
}
29 changes: 6 additions & 23 deletions src/Transformer/PropertyTransformer/PropertyTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,43 +10,25 @@
use AutoMapper\Transformer\TransformerInterface;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt;
use PhpParser\Parser;
use PhpParser\ParserFactory;

/**
* @internal
*/
final readonly class PropertyTransformer implements TransformerInterface, AllowNullValueTransformerInterface
final class PropertyTransformer implements TransformerInterface, AllowNullValueTransformerInterface
{
private Parser $parser;

/**
* @param array<mixed> $extraContext
*/
public function __construct(
private string $propertyTransformerId,
private array $extraContext = [],
?Parser $parser = null,
private readonly string $propertyTransformerId,
private ?Expr $computedValueExpr = null,
) {
$this->parser = $parser ?? (new ParserFactory())->createForHostVersion();
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr $source, ?Expr $existingValue = null): array
{
$context = new Expr\Variable('context');

if ($this->extraContext) {
$expr = $this->parser->parse('<?php ' . var_export($this->extraContext, true) . ';')[0] ?? null;

if ($expr instanceof Stmt\Expression) {
$context = new Expr\BinaryOp\Plus(
$context,
$expr->expr
);
}
}
$computeValueExpr = $this->computedValueExpr ?? new Expr\ConstFetch(new Name('null'));

$statements = [];
$transformExpr = new Expr\MethodCall(
Expand All @@ -58,6 +40,7 @@ public function transform(Expr $input, Expr $target, PropertyMetadata $propertyM
new Arg($input),
new Arg($source),
new Arg($context),
new Arg($computeValueExpr),
]
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Transformer\PropertyTransformer;

use AutoMapper\Metadata\MapperMetadata;
use AutoMapper\Metadata\SourcePropertyMetadata;
use AutoMapper\Metadata\TargetPropertyMetadata;

interface PropertyTransformerComputeInterface extends PropertyTransformerSupportInterface
{
/**
* When implemented with a PropertyTransformerSupportInterface, this method is called to compute a value that would be passed to the `transform` method.
*
* This value is exported by using `var_export` and used directly in the generated code.
*
* @param SourcePropertyMetadata $source The source property metadata
* @param TargetPropertyMetadata $target The target property metadata
* @param MapperMetadata $mapperMetadata The mapper metadata
*/
public function compute(SourcePropertyMetadata $source, TargetPropertyMetadata $target, MapperMetadata $mapperMetadata): mixed;

/**
* @param mixed $value the value of the property to transform, can be null if there is no way to read the data from the mapping
* @param object|array<string, mixed> $source the source input on which the custom transformation applies
* @param array<string, mixed> $context Context during mapping
* @param mixed $computed The computed value from PropertyTransformerComputeInterface, if applicable, otherwise null
*/
public function transform(mixed $value, object|array $source, array $context, mixed $computed = null): mixed;
}
15 changes: 15 additions & 0 deletions src/Transformer/PropertyTransformer/PropertyTransformerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
use AutoMapper\Transformer\PrioritizedTransformerFactoryInterface;
use AutoMapper\Transformer\TransformerFactoryInterface;
use AutoMapper\Transformer\TransformerInterface;
use PhpParser\Node\Stmt;
use PhpParser\Parser;
use PhpParser\ParserFactory;

/**
* @internal
Expand All @@ -19,10 +22,14 @@ final class PropertyTransformerFactory implements PrioritizedTransformerFactoryI
/** @var array<string, PropertyTransformerSupportInterface>|null */
private $prioritizedPropertyTransformers;

private Parser $parser;

public function __construct(
/** @var iterable<string, PropertyTransformerSupportInterface> */
private readonly iterable $propertyTransformersSupportList,
?Parser $parser = null,
) {
$this->parser = $parser ?? (new ParserFactory())->createForHostVersion();
}

public function getPriority(): int
Expand All @@ -34,6 +41,14 @@ public function getTransformer(SourcePropertyMetadata $source, TargetPropertyMet
{
foreach ($this->prioritizedPropertyTransformers() as $id => $propertyTransformer) {
if ($propertyTransformer instanceof PropertyTransformerSupportInterface && $propertyTransformer->supports($source, $target, $mapperMetadata)) {
if ($propertyTransformer instanceof PropertyTransformerComputeInterface) {
$computedValueCode = $propertyTransformer->compute($source, $target, $mapperMetadata);
$stmts = $this->parser->parse('<?php ' . var_export($computedValueCode, true) . ';');
$computedValueExpr = $stmts && $stmts[0] instanceof Stmt\Expression ? $stmts[0]->expr : null;

return new PropertyTransformer($id, $computedValueExpr);
}

return new PropertyTransformer($id);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*
* @experimental
*/
interface PropertyTransformerSupportInterface
interface PropertyTransformerSupportInterface extends PropertyTransformerInterface
{
/**
* When implemented with a PropertyTransformerInterface, this method is called to check if the transformer supports the given properties.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
AutoMapper\Tests\AutoMapperTest\TransformerWithComputedValue\Foo {
+foo: "computed value"
}
59 changes: 59 additions & 0 deletions tests/AutoMapperTest/TransformerWithComputedValue/map.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\AutoMapperTest\TransformerWithComputedValue;

use AutoMapper\Metadata\MapperMetadata;
use AutoMapper\Metadata\SourcePropertyMetadata;
use AutoMapper\Metadata\TargetPropertyMetadata;
use AutoMapper\Tests\AutoMapperBuilder;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerComputeInterface;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerSupportInterface;

class FooDto
{
public string $foo;
}

class Foo
{
public string $foo;
}

class ComputeValueTransformer implements PropertyTransformerInterface, PropertyTransformerSupportInterface, PropertyTransformerComputeInterface
{
public function transform(mixed $value, object|array $source, array $context, mixed $computed = null): mixed
{
return $computed ?? $value;
}

public function supports(
SourcePropertyMetadata $source,
TargetPropertyMetadata $target,
MapperMetadata $mapperMetadata,
): bool {
return true;
}

public function compute(
SourcePropertyMetadata $source,
TargetPropertyMetadata $target,
MapperMetadata $mapperMetadata,
): mixed {
return 'computed value';
}
}

$fooDto = new FooDto();
$fooDto->foo = 'original value';

$mapper = AutoMapperBuilder::buildAutoMapper(
propertyTransformers: [new ComputeValueTransformer()]
);

return $mapper->map(
$fooDto,
Foo::class,
);