Skip to content

feat(mapper): allow to merge existing values by extracting identifiers #260

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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 src/Attribute/MapFrom.php
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
* @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
* @param bool|null $identifier If true, the property will be used as an identifier
*/
public function __construct(
public string|array|null $source = null,
@@ -32,6 +33,7 @@ public function __construct(
public int $priority = 0,
public ?string $dateTimeFormat = null,
public ?bool $extractTypesFromGetter = null,
public ?bool $identifier = null,
) {
}
}
2 changes: 2 additions & 0 deletions src/Attribute/MapTo.php
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
* @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
* @param bool|null $identifier If true, the property will be used as an identifier
*/
public function __construct(
public string|array|null $target = null,
@@ -32,6 +33,7 @@ public function __construct(
public int $priority = 0,
public ?string $dateTimeFormat = null,
public ?bool $extractTypesFromGetter = null,
public ?bool $identifier = null,
) {
}
}
1 change: 1 addition & 0 deletions src/Event/PropertyMetadataEvent.php
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ public function __construct(
public int $priority = 0,
public readonly bool $isFromDefaultExtractor = false,
public ?bool $extractTypesFromGetter = null,
public ?bool $identifier = null,
) {
}
}
1 change: 1 addition & 0 deletions src/EventListener/MapFromListener.php
Original file line number Diff line number Diff line change
@@ -84,6 +84,7 @@ private function addPropertyFromTarget(GenerateMapperEvent $event, MapFrom $mapF
groups: $mapFrom->groups,
priority: $mapFrom->priority,
extractTypesFromGetter: $mapFrom->extractTypesFromGetter,
identifier: $mapFrom->identifier,
);

if (\array_key_exists($propertyMetadata->target->property, $event->properties) && $event->properties[$propertyMetadata->target->property]->priority >= $propertyMetadata->priority) {
1 change: 1 addition & 0 deletions src/EventListener/MapToListener.php
Original file line number Diff line number Diff line change
@@ -85,6 +85,7 @@ private function addPropertyFromSource(GenerateMapperEvent $event, MapTo $mapTo,
groups: $mapTo->groups,
priority: $mapTo->priority,
extractTypesFromGetter: $mapTo->extractTypesFromGetter,
identifier: $mapTo->identifier,
);

if (\array_key_exists($propertyMetadata->target->property, $event->properties) && $event->properties[$propertyMetadata->target->property]->priority >= $propertyMetadata->priority) {
37 changes: 22 additions & 15 deletions src/Extractor/ReadAccessor.php
Original file line number Diff line number Diff line change
@@ -32,17 +32,24 @@ final class ReadAccessor
public const TYPE_SOURCE = 4;
public const TYPE_ARRAY_ACCESS = 5;

public const EXTRACT_IS_UNDEFINED_CALLBACK = 'extractIsUndefinedCallbacks';
public const EXTRACT_IS_NULL_CALLBACK = 'extractIsNullCallbacks';
public const EXTRACT_CALLBACK = 'extractCallbacks';
public const EXTRACT_TARGET_IS_UNDEFINED_CALLBACK = 'extractTargetIsUndefinedCallbacks';
public const EXTRACT_TARGET_IS_NULL_CALLBACK = 'extractTargetIsNullCallbacks';
public const EXTRACT_TARGET_CALLBACK = 'extractTargetCallbacks';

/**
* @param array<string, string> $context
*/
public function __construct(
private readonly int $type,
private readonly string $accessor,
private readonly ?string $sourceClass = null,
private readonly bool $private = false,
private readonly ?string $property = null,
public readonly int $type,
public readonly string $accessor,
public readonly ?string $sourceClass = null,
public readonly bool $private = false,
public readonly ?string $property = null,
// will be the name of the property if different from accessor
private readonly array $context = [],
public readonly array $context = [],
) {
if (self::TYPE_METHOD === $this->type && null === $this->sourceClass) {
throw new InvalidArgumentException('Source class must be provided when using "method" type.');
@@ -54,7 +61,7 @@ public function __construct(
*
* @throws CompileException
*/
public function getExpression(Expr $input): Expr
public function getExpression(Expr $input, bool $target = false): Expr
{
if (self::TYPE_METHOD === $this->type) {
$methodCallArguments = [];
@@ -99,7 +106,7 @@ public function getExpression(Expr $input): Expr
* $this->extractCallbacks['method_name']($input)
*/
return new Expr\FuncCall(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->property ?? $this->accessor)),
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), $target ? self::EXTRACT_TARGET_CALLBACK : self::EXTRACT_CALLBACK), new Scalar\String_($this->property ?? $this->accessor)),
[
new Arg($input),
]
@@ -124,7 +131,7 @@ public function getExpression(Expr $input): Expr
* $this->extractCallbacks['property_name']($input)
*/
return new Expr\FuncCall(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->accessor)),
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), $target ? self::EXTRACT_TARGET_CALLBACK : self::EXTRACT_CALLBACK), new Scalar\String_($this->accessor)),
[
new Arg($input),
]
@@ -155,7 +162,7 @@ public function getExpression(Expr $input): Expr
throw new CompileException('Invalid accessor for read expression');
}

public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = false): ?Expr
public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = false, bool $target = false): ?Expr
{
// It is not possible to check if the underlying data is defined, assumes it is, php will throw an error if it is not
if (!$nullable && \in_array($this->type, [self::TYPE_METHOD, self::TYPE_SOURCE])) {
@@ -172,7 +179,7 @@ public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = fa
* !$this->extractIsUndefinedCallbacks['property_name']($input)
*/
return new Expr\BooleanNot(new Expr\FuncCall(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsUndefinedCallbacks'), new Scalar\String_($this->accessor)),
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), $target ? self::EXTRACT_TARGET_IS_UNDEFINED_CALLBACK : self::EXTRACT_IS_UNDEFINED_CALLBACK), new Scalar\String_($this->accessor)),
[
new Arg($input),
]
@@ -212,7 +219,7 @@ public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = fa
return null;
}

public function getIsNullExpression(Expr\Variable $input): Expr
public function getIsNullExpression(Expr\Variable $input, bool $target = false): Expr
{
if (self::TYPE_METHOD === $this->type) {
$methodCallExpr = $this->getExpression($input);
@@ -236,7 +243,7 @@ public function getIsNullExpression(Expr\Variable $input): Expr
* $this->extractIsNullCallbacks['property_name']($input)
*/
return new Expr\FuncCall(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsNullCallbacks'), new Scalar\String_($this->accessor)),
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), $target ? self::EXTRACT_TARGET_IS_NULL_CALLBACK : self::EXTRACT_IS_NULL_CALLBACK), new Scalar\String_($this->accessor)),
[
new Arg($input),
]
@@ -270,7 +277,7 @@ public function getIsNullExpression(Expr\Variable $input): Expr
throw new CompileException('Invalid accessor for read expression');
}

public function getIsUndefinedExpression(Expr\Variable $input): Expr
public function getIsUndefinedExpression(Expr\Variable $input, bool $target = false): Expr
{
if (\in_array($this->type, [self::TYPE_METHOD, self::TYPE_SOURCE])) {
/*
@@ -289,7 +296,7 @@ public function getIsUndefinedExpression(Expr\Variable $input): Expr
* $this->extractIsUndefinedCallbacks['property_name']($input)
*/
return new Expr\FuncCall(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsUndefinedCallbacks'), new Scalar\String_($this->accessor)),
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), $target ? self::EXTRACT_TARGET_IS_UNDEFINED_CALLBACK : self::EXTRACT_IS_UNDEFINED_CALLBACK), new Scalar\String_($this->accessor)),
[
new Arg($input),
]
4 changes: 2 additions & 2 deletions src/Extractor/WriteMutator.php
Original file line number Diff line number Diff line change
@@ -32,8 +32,8 @@ final class WriteMutator

public function __construct(
public readonly int $type,
private readonly string $property,
private readonly bool $private = false,
public readonly string $property,
public readonly bool $private = false,
public readonly ?\ReflectionParameter $parameter = null,
private readonly ?string $removeMethodName = null,
) {
19 changes: 19 additions & 0 deletions src/GeneratedMapper.php
Original file line number Diff line number Diff line change
@@ -36,6 +36,16 @@ public function registerMappers(AutoMapperRegistryInterface $registry): void
{
}

public function getSourceHash(mixed $value): ?string
{
return null;
}

public function getTargetHash(mixed $value): ?string
{
return null;
}

/** @var array<string, MapperInterface<object, object>|MapperInterface<object, array<mixed>>|MapperInterface<array<mixed>, object>> */
protected array $mappers = [];

@@ -51,6 +61,15 @@ public function registerMappers(AutoMapperRegistryInterface $registry): void
/** @var array<string, callable(): bool>) */
protected array $extractIsUndefinedCallbacks = [];

/** @var array<string, callable(): mixed> */
protected array $extractTargetCallbacks = [];

/** @var array<string, callable(): bool>) */
protected array $extractTargetIsNullCallbacks = [];

/** @var array<string, callable(): bool>) */
protected array $extractTargetIsUndefinedCallbacks = [];

/** @var Target|\ReflectionClass<object> */
protected mixed $cachedTarget;
}
100 changes: 100 additions & 0 deletions src/Generator/IdentifierHashGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Generator;

use AutoMapper\Metadata\GeneratorMetadata;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt;

final readonly class IdentifierHashGenerator
{
/**
* @return list<Stmt>
*/
public function getStatements(GeneratorMetadata $metadata, bool $fromSource): array
{
$identifiers = [];

foreach ($metadata->propertiesMetadata as $propertyMetadata) {
if (!$propertyMetadata->identifier) {
continue;
}

if (null === $propertyMetadata->target->readAccessor) {
continue;
}

if (null === $propertyMetadata->source->accessor) {
continue;
}

$identifiers[] = $propertyMetadata;
}

if (empty($identifiers)) {
return [];
}

$hashCtxVariable = new Expr\Variable('hashCtx');

$statements = [
new Stmt\Expression(new Expr\Assign($hashCtxVariable, new Expr\FuncCall(new Name('hash_init'), [
new Arg(new Scalar\String_('sha256')),
]))),
];

$valueVariable = new Expr\Variable('value');

// foreach property we check
foreach ($identifiers as $property) {
if (null === $property->source->accessor || null === $property->target->readAccessor) {
continue;
}

// check if the source is defined
if ($fromSource) {
if ($property->source->checkExists) {
$statements[] = new Stmt\If_($property->source->accessor->getIsUndefinedExpression($valueVariable), [
'stmts' => [
new Stmt\Return_(new Expr\ConstFetch(new Name('null'))),
],
]);
}

// add identifier to hash
$statements[] = new Stmt\Expression(new Expr\FuncCall(new Name('hash_update'), [
new Arg($hashCtxVariable),
new Arg($property->source->accessor->getExpression($valueVariable)),
]));
} else {
$statements[] = new Stmt\If_($property->target->readAccessor->getIsUndefinedExpression($valueVariable, true), [
'stmts' => [
new Stmt\Return_(new Expr\ConstFetch(new Name('null'))),
],
]);

$statements[] = new Stmt\Expression(new Expr\FuncCall(new Name('hash_update'), [
new Arg($hashCtxVariable),
new Arg($property->target->readAccessor->getExpression($valueVariable, true)),
]));
}
}

if (\count($statements) < 2) {
return [];
}

// return hash as string
$statements[] = new Stmt\Return_(new Expr\FuncCall(new Name('hash_final'), [
new Arg($hashCtxVariable),
new Arg(new Scalar\String_('true')),
]));

return $statements;
}
}
Loading