-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace AutoMapper\Attribute; | ||
|
||
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] | ||
final readonly class MapTo | ||
{ | ||
/** | ||
* @param non-empty-string $propertyName | ||
* @param 'array'|class-string|null $target | ||
*/ | ||
public function __construct( | ||
public string $propertyName, | ||
public string|null $target = null, | ||
) | ||
{ | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace AutoMapper\CustomTransformer; | ||
|
||
use AutoMapper\Extractor\AstExtractor; | ||
use AutoMapper\Extractor\ReadAccessor; | ||
use PhpParser\Builder; | ||
use PhpParser\Node\Expr; | ||
use PhpParser\Node\Scalar\String_; | ||
use PhpParser\Node\Stmt; | ||
use PhpParser\PrettyPrinter\Standard; | ||
|
||
final readonly class CustomTransformerGenerator | ||
{ | ||
private AstExtractor $astExtractor; | ||
|
||
public function __construct( | ||
private CustomTransformersRegistry $customTransformerRegistry, | ||
AstExtractor|null $astExtractor = null | ||
) { | ||
$this->astExtractor = $astExtractor ?? new AstExtractor(); | ||
} | ||
|
||
/** | ||
* @return class-string<CustomTransformerInterface> | ||
*/ | ||
public function generateMapToCustomTransformer( | ||
string $source, | ||
string $target, | ||
string $sourceProperty, | ||
string $targetProperty, | ||
ReadAccessor $readAccessor | ||
): string { | ||
$transformerClass = strtr('MapTo_Transformer_{source}_{target}_{sourceProperty}_{targetProperty}', [ | ||
'{source}' => str_replace('\\', '_', $source), | ||
'{target}' => str_replace('\\', '_', $target), | ||
'{sourceProperty}' => $sourceProperty, | ||
'{targetProperty}' => $targetProperty, | ||
]); | ||
|
||
$file = __DIR__ . "/{$transformerClass}.php"; | ||
|
||
if (class_exists($transformerClass)) { | ||
$this->customTransformerRegistry->addCustomTransformer(new $transformerClass()); | ||
Check failure on line 46 in src/CustomTransformer/CustomTransformerGenerator.php
|
||
|
||
return $transformerClass; | ||
Check failure on line 48 in src/CustomTransformer/CustomTransformerGenerator.php
|
||
} | ||
|
||
if (!file_exists($file)) { | ||
$class = (new Builder\Class_($transformerClass)) | ||
->makeFinal() | ||
->makeReadonly() | ||
->implement(CustomPropertyTransformerInterface::class) | ||
->addStmt($this->generateSupportsStatement($source, $target, $targetProperty)) | ||
->addStmt($this->generateTransformStatement($readAccessor, $source)) | ||
->getNode(); | ||
|
||
$code = "<?php\n" . (new Standard())->prettyPrint([$class]); | ||
file_put_contents($file = __DIR__ . "/generated_transformers/{$transformerClass}.php", $code); | ||
} | ||
|
||
require_once $file; | ||
|
||
$this->customTransformerRegistry->addCustomTransformer(new $transformerClass()); | ||
Check failure on line 66 in src/CustomTransformer/CustomTransformerGenerator.php
|
||
|
||
return $transformerClass; | ||
Check failure on line 68 in src/CustomTransformer/CustomTransformerGenerator.php
|
||
} | ||
|
||
/** | ||
* public function supports(string $source, string $target, string $propertyName): bool | ||
* { | ||
* return $source === [source type] && $target === [target type] && $propertyName === [target property]; | ||
* } | ||
*/ | ||
private function generateSupportsStatement(string $source, string $target, string $targetProperty): Stmt\ClassMethod | ||
{ | ||
$supportsMethodInInterface = $this->astExtractor | ||
->extractClassLike(CustomPropertyTransformerInterface::class) | ||
->getMethod('supports') ?? throw new \LogicException( | ||
'Cannot find "supports" method in interface ' . CustomPropertyTransformerInterface::class | ||
); | ||
|
||
return (new Builder\Method('supports')) | ||
->makePublic() | ||
->setReturnType($supportsMethodInInterface->getReturnType()) | ||
Check failure on line 87 in src/CustomTransformer/CustomTransformerGenerator.php
|
||
->addParams($supportsMethodInInterface->getParams()) | ||
->addStmt( | ||
new Stmt\Return_( | ||
new Expr\BinaryOp\BooleanAnd( | ||
new Expr\BinaryOp\Identical( | ||
$supportsMethodInInterface->getParams()[0]->var, | ||
new String_($source) | ||
), | ||
new Expr\BinaryOp\BooleanAnd( | ||
new Expr\BinaryOp\Identical( | ||
$supportsMethodInInterface->getParams()[1]->var, | ||
new String_($target) | ||
), | ||
new Expr\BinaryOp\Identical( | ||
$supportsMethodInInterface->getParams()[2]->var, | ||
new String_($targetProperty) | ||
), | ||
) | ||
) | ||
) | ||
) | ||
->getNode(); | ||
} | ||
|
||
/** | ||
* public function transform(object|array $source): mixed | ||
* { | ||
* return $source->[sourceProperty accessor]; | ||
* } | ||
*/ | ||
private function generateTransformStatement(ReadAccessor $readAccessor, string $source): Stmt\ClassMethod | ||
{ | ||
$transformMethodInInterface = $this->astExtractor | ||
->extractClassLike(CustomTransformerInterface::class) | ||
->getMethod('transform') ?? throw new \LogicException( | ||
'Cannot find "transform" method in interface ' . CustomTransformerInterface::class | ||
); | ||
|
||
return (new Builder\Method('transform')) | ||
->makePublic() | ||
->setReturnType($transformMethodInInterface->getReturnType()) | ||
Check failure on line 128 in src/CustomTransformer/CustomTransformerGenerator.php
|
||
->setDocComment( | ||
<<<PHPDOC | ||
/** | ||
* @param $source \$source | ||
*/ | ||
PHPDOC | ||
) | ||
->addParams($transformMethodInInterface->getParams()) | ||
->addStmt( | ||
new Stmt\Return_( | ||
$readAccessor->getExpression($transformMethodInInterface->getParams()[0]->var) | ||
Check failure on line 139 in src/CustomTransformer/CustomTransformerGenerator.php
|
||
) | ||
) | ||
->getNode(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,7 @@ | |
|
||
declare(strict_types=1); | ||
|
||
namespace AutoMapper\Transformer\CustomTransformer; | ||
namespace AutoMapper\CustomTransformer; | ||
|
||
use AutoMapper\MapperMetadataInterface; | ||
use Symfony\Component\PropertyInfo\Type; | ||
|
@@ -12,16 +12,17 @@ | |
*/ | ||
final class CustomTransformersRegistry | ||
{ | ||
/** @var list<CustomTransformerInterface> */ | ||
/** @var CustomTransformerInterface */ | ||
private array $customTransformers = []; | ||
Check failure on line 16 in src/CustomTransformer/CustomTransformersRegistry.php
|
||
|
||
/** @var list<CustomTransformerInterface>|null */ | ||
/** @var CustomTransformerInterface|null */ | ||
private array|null $prioritizedCustomTransformers = null; | ||
Check failure on line 19 in src/CustomTransformer/CustomTransformersRegistry.php
|
||
|
||
public function addCustomTransformer(CustomTransformerInterface $customTransformer): void | ||
{ | ||
if (!\in_array($customTransformer, $this->customTransformers, true)) { | ||
$this->customTransformers[] = $customTransformer; | ||
$this->prioritizedCustomTransformers = null; // reset priority computation | ||
} | ||
} | ||
|
||
|
@@ -46,7 +47,7 @@ public function getCustomTransformerClass(MapperMetadataInterface $mapperMetadat | |
} | ||
|
||
/** | ||
* @return list<CustomTransformerInterface> | ||
* @return CustomTransformerInterface | ||
*/ | ||
private function prioritizedCustomTransformers(): array | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace AutoMapper\Extractor; | ||
|
||
use AutoMapper\Exception\InvalidArgumentException; | ||
use AutoMapper\Exception\RuntimeException; | ||
use PhpParser\Node; | ||
use PhpParser\NodeTraverser; | ||
use PhpParser\NodeVisitor\NameResolver; | ||
use PhpParser\Parser; | ||
use PhpParser\ParserFactory; | ||
|
||
final readonly class AstExtractor | ||
{ | ||
private Parser $parser; | ||
|
||
public function __construct(?Parser $parser = null) | ||
{ | ||
$this->parser = $parser ?? (new ParserFactory())->create(ParserFactory::PREFER_PHP7); | ||
} | ||
|
||
public function extractClassLike(string $class, bool $resolveImports = false): Node\Stmt\ClassLike | ||
{ | ||
$fileName = (new \ReflectionClass($class))->getFileName(); | ||
if (false === $fileName) { | ||
throw new RuntimeException("You cannot extract code from \"{$class}\" class."); | ||
} | ||
$fileContents = file_get_contents($fileName); | ||
if (false === $fileContents) { | ||
throw new RuntimeException("File \"{$fileName}\" for \"{$class}\" couldn't be read."); | ||
} | ||
|
||
$statements = $this->parser->parse($fileContents); | ||
|
||
if (null === $statements) { | ||
throw new RuntimeException("Couldn't parse file \"{$fileName}\" for class \"{$class}\"."); | ||
} | ||
|
||
if ($resolveImports) { | ||
$statements = $this->resolveFullyQualifiedClassNames($statements); | ||
} | ||
|
||
try { | ||
$namespaceStatement = self::findUnique(Node\Stmt\Namespace_::class, $statements, $fileName); | ||
} catch (\Exception) { | ||
// if no namespace, directly search for a class | ||
return self::findUnique(Node\Stmt\ClassLike::class, $statements, $fileName); | ||
} | ||
|
||
return self::findUnique(Node\Stmt\ClassLike::class, $namespaceStatement->stmts, $fileName); | ||
} | ||
|
||
/** | ||
* @template T of Node | ||
* | ||
* @param class-string<T> $searchedStatementClass | ||
* @param Node[] $statements | ||
* | ||
* @return T | ||
*/ | ||
private static function findUnique(string $searchedStatementClass, array $statements, string $fileName): Node | ||
{ | ||
$foundStatements = array_filter( | ||
$statements, | ||
static fn(Node $statement): bool => $statement instanceof $searchedStatementClass, | ||
); | ||
|
||
if (\count($foundStatements) > 1) { | ||
throw new InvalidArgumentException("Multiple \"{$searchedStatementClass}\" found in file \"{$fileName}\"."); | ||
} | ||
|
||
return array_values($foundStatements)[0] ?? throw new InvalidArgumentException( | ||
"No \"{$searchedStatementClass}\" found in file \"{$fileName}\"." | ||
); | ||
} | ||
|
||
/** | ||
* Transform all statements with imported class names, into FQCNs. | ||
* | ||
* @param Node[] $statements | ||
* | ||
* @return Node[] | ||
*/ | ||
private function resolveFullyQualifiedClassNames(array $statements): array | ||
{ | ||
$nodeTraverser = new NodeTraverser(); | ||
$nodeTraverser->addVisitor(new NameResolver()); | ||
|
||
return $nodeTraverser->traverse($statements); | ||
} | ||
} |