diff --git a/src/Exception/CircularReferenceException.php b/src/Exception/CircularReferenceException.php index b363860f..268f4414 100644 --- a/src/Exception/CircularReferenceException.php +++ b/src/Exception/CircularReferenceException.php @@ -2,15 +2,6 @@ declare(strict_types=1); -/* - * This file is part of the Symfony package. - * - * (c) Fabien Potencier - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace AutoMapper\Exception; /** diff --git a/src/Exception/CompileException.php b/src/Exception/CompileException.php index f57e3cd6..ebde8551 100644 --- a/src/Exception/CompileException.php +++ b/src/Exception/CompileException.php @@ -2,15 +2,6 @@ declare(strict_types=1); -/* - * This file is part of the Symfony package. - * - * (c) Fabien Potencier - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace AutoMapper\Exception; /** diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..2adacb52 --- /dev/null +++ b/src/Exception/InvalidArgumentException.php @@ -0,0 +1,12 @@ + + */ +final class InvalidArgumentException extends \InvalidArgumentException +{ +} diff --git a/src/Exception/InvalidMappingException.php b/src/Exception/InvalidMappingException.php index db3dfdcc..071260ba 100644 --- a/src/Exception/InvalidMappingException.php +++ b/src/Exception/InvalidMappingException.php @@ -2,15 +2,6 @@ declare(strict_types=1); -/* - * This file is part of the Symfony package. - * - * (c) Fabien Potencier - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace AutoMapper\Exception; /** diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php new file mode 100644 index 00000000..183ec959 --- /dev/null +++ b/src/Exception/LogicException.php @@ -0,0 +1,12 @@ + + */ +final class LogicException extends \LogicException +{ +} diff --git a/src/Exception/NoMappingFoundException.php b/src/Exception/NoMappingFoundException.php index ba8db22c..a581cc9c 100644 --- a/src/Exception/NoMappingFoundException.php +++ b/src/Exception/NoMappingFoundException.php @@ -2,15 +2,6 @@ declare(strict_types=1); -/* - * This file is part of the Symfony package. - * - * (c) Fabien Potencier - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace AutoMapper\Exception; /** diff --git a/src/Extractor/AstExtractor.php b/src/Extractor/AstExtractor.php new file mode 100644 index 00000000..842306fa --- /dev/null +++ b/src/Extractor/AstExtractor.php @@ -0,0 +1,114 @@ + + * @author Baptiste Leduc + * + * @internal + */ +final readonly class AstExtractor +{ + private Parser $parser; + + public function __construct(?Parser $parser = null) + { + $this->parser = $parser ?? (new ParserFactory())->create(ParserFactory::PREFER_PHP7); + } + + /** + * Extracts the code of the given method from a given class, and wraps it inside a closure, in order to inject it + * in the generated mappers. + * + * @param class-string $class + * @param Arg[] $inputParameters + */ + public function extract(string $class, string $method, array $inputParameters): Expr + { + $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}\"."); + } + + $namespaceStatement = self::findUnique(Stmt\Namespace_::class, $statements, $fileName); + /** @var Stmt\Class_ $classStatement */ + $classStatement = self::findUnique(Stmt\Class_::class, $namespaceStatement->stmts, $fileName); + + $classMethod = $classStatement->getMethod($method) ?? throw new LogicException("Cannot find method \"{$method}()\" in class \"{$class}\"."); + + if (\count($inputParameters) !== \count($classMethod->getParams())) { + throw new InvalidArgumentException("Input parameters and method parameters in class \"{$class}\" do not match."); + } + + foreach ($classMethod->getParams() as $key => $parameter) { + /** @var Expr\Variable $inputParameterValue */ + $inputParameterValue = $inputParameters[$key]->value; + + if ($parameter->var instanceof Expr\Variable && $inputParameterValue->name !== $parameter->var->name) { + $parameterName = \is_string($parameter->var->name) ? $parameter->var->name : 'N/A'; + throw new InvalidArgumentException("Method parameter \"{$parameterName}\" does not match type \"{$inputParameters[$key]->getType()}\" from input parameter \"{$inputParameters[$key]->name}\" in \"{$class}::{$method}\" method."); + } + } + + $closureParameters = []; + foreach ($classMethod->getParams() as $parameter) { + if ($parameter->var instanceof Expr\Variable && $parameter->type instanceof Identifier) { + $closureParameters[] = new Param(new Expr\Variable($parameter->var->name), type: $parameter->type->name); + } + } + + return new Expr\FuncCall( + new Expr\Closure([ + 'stmts' => $classMethod->stmts, + 'params' => $closureParameters, + 'returnType' => $classMethod->returnType, + ]), + $inputParameters, + ); + } + + /** + * @template T of Stmt + * + * @param class-string $searchedStatementClass + * @param Stmt[] $statements + * + * @return T + */ + private static function findUnique(string $searchedStatementClass, array $statements, string $fileName): Stmt + { + $foundStatements = array_filter( + $statements, + static fn (Stmt $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}\"."); + } +} diff --git a/tests/Extractor/AstExtractorTest.php b/tests/Extractor/AstExtractorTest.php new file mode 100644 index 00000000..8e5a363e --- /dev/null +++ b/tests/Extractor/AstExtractorTest.php @@ -0,0 +1,89 @@ + + */ +class AstExtractorTest extends TestCase +{ + public function testExtractSimpleMethod(): void + { + $extractor = new AstExtractor(); + $extractedMethod = new Expression($extractor->extract(FooCustomMapper::class, 'transform', [new Arg(new Variable('object'))])); + + $this->assertEquals(<<bar = 'Hello World!'; + } + return \$object; +})(\$object); +PHP, $generatedCode = (new Standard())->prettyPrint([$extractedMethod])); + + $codeToEval = <<bar = 'Hello'; + +{$generatedCode} + +return \$object; +PHP; + + /** @var Foo $object */ + $object = eval($codeToEval); + $this->assertEquals('Hello World!', $object->bar); + } + + public function testCannotExtractCode(): void + { + $coreClass = \Generator::class; + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("You cannot extract code from \"{$coreClass}\" class."); + + $extractor = new AstExtractor(); + $extractor->extract($coreClass, 'rewind', [new Arg(new Variable('object'))]); + } + + public function testInvalidInputParameters(): void + { + $class = FooCustomMapper::class; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Input parameters and method parameters in class \"{$class}\" do not match."); + + $extractor = new AstExtractor(); + $extractor->extract($class, 'transform', [new Arg(new Variable('object')), new Arg(new Variable('context'))]); + } + + public function testInvalidExtractedMethodParameters(): void + { + $class = FooCustomMapper::class; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Input parameters and method parameters in class \"{$class}\" do not match."); + + $extractor = new AstExtractor(); + $extractor->extract($class, 'switch', [new Arg(new Variable('object'))]); + } +} diff --git a/tests/Extractor/Fixtures/Foo.php b/tests/Extractor/Fixtures/Foo.php new file mode 100644 index 00000000..c3910968 --- /dev/null +++ b/tests/Extractor/Fixtures/Foo.php @@ -0,0 +1,11 @@ +bar = 'Hello World!'; + } + + return $object; + } + + public function switch(mixed $object, array $context): mixed + { + if ($object instanceof Foo) { + $object->bar = 'Hello World!'; + } + + return $object; + } +}