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: extra target values #229

Merged
merged 6 commits into from
Oct 13, 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CHANGELOG

## 1.10.1
## 1.11.0

* refactor: spin off `resolveTargetClass()` to separate class
* perf: proxy warming
Expand All @@ -11,6 +11,7 @@
* perf: `ObjectToObjectTransformer` optimization
* perf: move `TypeResolver` outside of the hot path
* perf(`ObjectCache`): use sentinel value instead of exception
* feat: extra target values

## 1.10.0

Expand Down
44 changes: 44 additions & 0 deletions src/Context/ExtraTargetValues.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?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\Context;

/**
* Adds additional values to the target object
*/
final readonly class ExtraTargetValues
{
/**
* @param array<class-string,array<string,mixed>> $arguments
*/
public function __construct(
private array $arguments = [],
) {}

/**
* @param list<class-string> $classes The class and its parent classes and interfaces.
* @return array<string,mixed>
*/
public function getArgumentsForClass(array $classes): array
{
$arguments = [];

foreach ($classes as $class) {
if (isset($this->arguments[$class])) {
$arguments = array_merge($arguments, $this->arguments[$class]);
}
}

return $arguments;
}
}
40 changes: 40 additions & 0 deletions src/Transformer/Exception/ExtraTargetPropertyNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?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\Transformer\Exception;

use Rekalogika\Mapper\Context\Context;
use Rekalogika\Mapper\Exception\LogicException;

/**
* @internal
*/
class ExtraTargetPropertyNotFoundException extends LogicException
{
/**
* @param class-string $class
*/
public function __construct(
string $class,
string $property,
Context $context,
) {
$message = \sprintf(
'Mapper is called with "ExtraTargetValues", but cannot find the target property "%s" in class "%s"',
$property,
$class,
);

parent::__construct($message, context: $context);
}
}
111 changes: 105 additions & 6 deletions src/Transformer/Implementation/ObjectToObjectTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Rekalogika\Mapper\CacheWarmer\WarmableObjectToObjectMetadataFactoryInterface;
use Rekalogika\Mapper\CacheWarmer\WarmableTransformerInterface;
use Rekalogika\Mapper\Context\Context;
use Rekalogika\Mapper\Context\ExtraTargetValues;
use Rekalogika\Mapper\Context\MapperOptions;
use Rekalogika\Mapper\Exception\InvalidArgumentException;
use Rekalogika\Mapper\ObjectCache\ObjectCache;
Expand All @@ -26,6 +27,7 @@
use Rekalogika\Mapper\ServiceMethod\ServiceMethodSpecification;
use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface;
use Rekalogika\Mapper\Transformer\Exception\ClassNotInstantiableException;
use Rekalogika\Mapper\Transformer\Exception\ExtraTargetPropertyNotFoundException;
use Rekalogika\Mapper\Transformer\Exception\InstantiationFailureException;
use Rekalogika\Mapper\Transformer\Exception\NotAClassException;
use Rekalogika\Mapper\Transformer\Exception\UninitializedSourcePropertyException;
Expand Down Expand Up @@ -123,6 +125,13 @@ public function transform(
$target = null;
}

// get extra target values

$extraTargetValues = $this->getExtraTargetValues(
objectToObjectMetadata: $objectToObjectMetadata,
context: $context,
);

// initialize target if target is null

if (null === $target) {
Expand All @@ -133,12 +142,14 @@ public function transform(
$target = $this->instantiateTargetProxy(
source: $source,
objectToObjectMetadata: $objectToObjectMetadata,
extraTargetValues: $extraTargetValues,
context: $context,
);
} else {
$target = $this->instantiateRealTarget(
source: $source,
objectToObjectMetadata: $objectToObjectMetadata,
extraTargetValues: $extraTargetValues,
context: $context,
);
}
Expand Down Expand Up @@ -180,16 +191,47 @@ public function transform(
source: $source,
target: $target,
propertyMappings: $objectToObjectMetadata->getPropertyMappings(),
extraTargetValues: $extraTargetValues,
context: $context,
);
}

return $target;
}

/**
* @return array<string,mixed>
*/
private function getExtraTargetValues(
ObjectToObjectMetadata $objectToObjectMetadata,
Context $context,
): array {
$extraTargetValues = $context(ExtraTargetValues::class)
?->getArgumentsForClass($objectToObjectMetadata->getAllTargetClasses())
?? [];

$allPropertyMappings = $objectToObjectMetadata->getPropertyMappings();

foreach (array_keys($extraTargetValues) as $property) {
if (!isset($allPropertyMappings[$property])) {
throw new ExtraTargetPropertyNotFoundException(
class: $objectToObjectMetadata->getTargetClass(),
property: $property,
context: $context,
);
}
}

return $extraTargetValues;
}

/**
* @param array<string,mixed> $extraTargetValues
*/
private function instantiateRealTarget(
object $source,
ObjectToObjectMetadata $objectToObjectMetadata,
array $extraTargetValues,
Context $context,
): object {
$targetClass = $objectToObjectMetadata->getTargetClass();
Expand All @@ -203,6 +245,7 @@ private function instantiateRealTarget(
$constructorArguments = $this->generateConstructorArguments(
source: $source,
objectToObjectMetadata: $objectToObjectMetadata,
extraTargetValues: $extraTargetValues,
context: $context,
);

Expand All @@ -223,9 +266,13 @@ private function instantiateRealTarget(
}
}

/**
* @param array<string,mixed> $extraTargetValues
*/
private function instantiateTargetProxy(
object $source,
ObjectToObjectMetadata $objectToObjectMetadata,
array $extraTargetValues,
Context $context,
): object {
$targetClass = $objectToObjectMetadata->getTargetClass();
Expand All @@ -243,6 +290,7 @@ private function instantiateTargetProxy(
$source,
$objectToObjectMetadata,
$context,
$extraTargetValues,
): void {
// if the constructor is lazy, run it here

Expand All @@ -251,6 +299,7 @@ private function instantiateTargetProxy(
source: $source,
target: $target,
objectToObjectMetadata: $objectToObjectMetadata,
extraTargetValues: $extraTargetValues,
context: $context,
);
}
Expand All @@ -261,6 +310,7 @@ private function instantiateTargetProxy(
source: $source,
target: $target,
propertyMappings: $objectToObjectMetadata->getLazyPropertyMappings(),
extraTargetValues: $extraTargetValues,
context: $context,
);
};
Expand All @@ -280,6 +330,7 @@ private function instantiateTargetProxy(
source: $source,
target: $target,
objectToObjectMetadata: $objectToObjectMetadata,
extraTargetValues: $extraTargetValues,
context: $context,
);
}
Expand All @@ -290,16 +341,21 @@ private function instantiateTargetProxy(
source: $source,
target: $target,
propertyMappings: $objectToObjectMetadata->getEagerPropertyMappings(),
extraTargetValues: $extraTargetValues,
context: $context,
);

return $target;
}

/**
* @param array<string,mixed> $extraTargetValues
*/
private function runConstructorManually(
object $source,
object $target,
ObjectToObjectMetadata $objectToObjectMetadata,
array $extraTargetValues,
Context $context,
): object {
if (!method_exists($target, '__construct')) {
Expand All @@ -309,6 +365,7 @@ private function runConstructorManually(
$constructorArguments = $this->generateConstructorArguments(
source: $source,
objectToObjectMetadata: $objectToObjectMetadata,
extraTargetValues: $extraTargetValues,
context: $context,
);

Expand All @@ -334,16 +391,22 @@ private function runConstructorManually(
return $target;
}

/**
* @param array<string,mixed> $extraTargetValues
*/
private function generateConstructorArguments(
object $source,
ObjectToObjectMetadata $objectToObjectMetadata,
array $extraTargetValues,
Context $context,
): ConstructorArguments {
$propertyMappings = $objectToObjectMetadata->getConstructorPropertyMappings();
$constructorPropertyMappings = $objectToObjectMetadata->getConstructorPropertyMappings();

$constructorArguments = new ConstructorArguments();

foreach ($propertyMappings as $propertyMapping) {
// add arguments from property mappings

foreach ($constructorPropertyMappings as $propertyMapping) {
try {
/** @var mixed $targetPropertyValue */
[$targetPropertyValue,] = $this->transformValue(
Expand Down Expand Up @@ -379,16 +442,30 @@ private function generateConstructorArguments(
}
}

// add arguments from extra target values

/** @var mixed $value */
foreach ($extraTargetValues as $property => $value) {
// skip if there is no constructor property mapping for this
if (!isset($constructorPropertyMappings[$property])) {
continue;
}

$constructorArguments->addArgument($property, $value);
}

return $constructorArguments;
}

/**
* @param array<int,PropertyMapping> $propertyMappings
* @param array<string,PropertyMapping> $propertyMappings
* @param array<string,mixed> $extraTargetValues
*/
private function readSourceAndWriteTarget(
object $source,
object $target,
array $propertyMappings,
array $extraTargetValues,
Context $context,
): object {
foreach ($propertyMappings as $propertyMapping) {
Expand All @@ -400,6 +477,25 @@ private function readSourceAndWriteTarget(
);
}

// process extra target values

/** @var mixed $value */
foreach ($extraTargetValues as $property => $value) {
if (!isset($propertyMappings[$property])) {
continue;
}

$propertyMapping = $propertyMappings[$property];

$target = $this->readerWriter->writeTargetProperty(
target: $target,
propertyMapping: $propertyMapping,
value: $value,
context: $context,
silentOnError: true,
);
}

return $target;
}

Expand Down Expand Up @@ -435,7 +531,10 @@ private function readSourcePropertyAndWriteTargetProperty(

// write

if ($isChanged) {
if (
$isChanged
|| $propertyMapping->getTargetSetterWriteMode() === WriteMode::DynamicProperty
) {
if ($targetPropertyValue instanceof AdderRemoverProxy) {
$target = $targetPropertyValue->getHostObject();
}
Expand All @@ -445,6 +544,7 @@ private function readSourcePropertyAndWriteTargetProperty(
propertyMapping: $propertyMapping,
value: $targetPropertyValue,
context: $context,
silentOnError: false,
);
}

Expand Down Expand Up @@ -611,8 +711,7 @@ private function transformValue(

return [
$targetPropertyValue,
$targetPropertyValue !== $originalTargetPropertyValue
|| $propertyMapping->getTargetSetterWriteMode() === WriteMode::DynamicProperty,
$targetPropertyValue !== $originalTargetPropertyValue,
];
}

Expand Down
Loading