diff --git a/CHANGELOG.md b/CHANGELOG.md index ffba5c2..c238b82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- [GH#180](https://github.com/jolicode/automapper/pull/180) Add configuration to generate code with strict types ## [9.1.2] - 2024-09-03 ### Fixed diff --git a/src/Attribute/Mapper.php b/src/Attribute/Mapper.php index 6899668..ec99e9d 100644 --- a/src/Attribute/Mapper.php +++ b/src/Attribute/Mapper.php @@ -23,6 +23,7 @@ public function __construct( public ?bool $checkAttributes = null, public ?ConstructorStrategy $constructorStrategy = null, public ?bool $allowReadOnlyTargetToPopulate = null, + public ?bool $strictTypes = null, public int $priority = 0, public ?string $dateTimeFormat = null, ) { diff --git a/src/Configuration.php b/src/Configuration.php index f17937a..b2e42e7 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -36,6 +36,10 @@ public function __construct( * Does the mapper should throw an exception if the target is read-only. */ public bool $allowReadOnlyTargetToPopulate = false, + /** + * Add declare(strict_types=1) to generated code. + */ + public bool $strictTypes = false, ) { } } diff --git a/src/Event/GenerateMapperEvent.php b/src/Event/GenerateMapperEvent.php index 2987d44..d1dda52 100644 --- a/src/Event/GenerateMapperEvent.php +++ b/src/Event/GenerateMapperEvent.php @@ -22,6 +22,7 @@ public function __construct( public ?bool $checkAttributes = null, public ?ConstructorStrategy $constructorStrategy = null, public ?bool $allowReadOnlyTargetToPopulate = null, + public ?bool $strictTypes = null, ) { } } diff --git a/src/EventListener/MapperListener.php b/src/EventListener/MapperListener.php index 84fed6c..91f2024 100644 --- a/src/EventListener/MapperListener.php +++ b/src/EventListener/MapperListener.php @@ -72,6 +72,7 @@ public function __invoke(GenerateMapperEvent $event): void $event->checkAttributes ??= $mapper->checkAttributes; $event->constructorStrategy ??= $mapper->constructorStrategy; $event->allowReadOnlyTargetToPopulate ??= $mapper->allowReadOnlyTargetToPopulate; + $event->strictTypes ??= $mapper->strictTypes; $event->mapperMetadata->dateTimeFormat = $mapper->dateTimeFormat; } } diff --git a/src/Generator/MapperGenerator.php b/src/Generator/MapperGenerator.php index e1e7436..f575d56 100644 --- a/src/Generator/MapperGenerator.php +++ b/src/Generator/MapperGenerator.php @@ -20,6 +20,9 @@ use PhpParser\Node\Stmt; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use function AutoMapper\PhpParser\create_declare_item; +use function AutoMapper\PhpParser\create_scalar_int; + /** * Generates code for a mapping class. * @@ -58,22 +61,31 @@ public function __construct( /** * Generate Class AST given metadata for a mapper. * + * @return Stmt[] + * * @throws CompileException * @throws InvalidMappingException */ - public function generate(GeneratorMetadata $metadata): Stmt\Class_ + public function generate(GeneratorMetadata $metadata): array { if ($this->disableGeneratedMapper) { throw new InvalidMappingException('No mapper found for source ' . $metadata->mapperMetadata->source . ' and target ' . $metadata->mapperMetadata->target); } - return (new Builder\Class_($metadata->mapperMetadata->className)) + $statements = []; + if ($metadata->strictTypes) { + // @phpstan-ignore argument.type + $statements[] = new Stmt\Declare_([create_declare_item('strict_types', create_scalar_int(1))]); + } + $statements[] = (new Builder\Class_($metadata->mapperMetadata->className)) ->makeFinal() ->extend(GeneratedMapper::class) ->addStmt($this->constructorMethod($metadata)) ->addStmt($this->mapMethod($metadata)) ->addStmt($this->registerMappersMethod($metadata)) ->getNode(); + + return $statements; } /** diff --git a/src/Loader/EvalLoader.php b/src/Loader/EvalLoader.php index 2b3c048..43f9b2c 100644 --- a/src/Loader/EvalLoader.php +++ b/src/Loader/EvalLoader.php @@ -31,9 +31,9 @@ public function __construct( public function loadClass(MapperMetadata $mapperMetadata): void { - $class = $this->generator->generate($this->metadataFactory->getGeneratorMetadata($mapperMetadata->source, $mapperMetadata->target)); - - eval($this->printer->prettyPrint([$class])); + eval($this->printer->prettyPrint($this->generator->generate( + $this->metadataFactory->getGeneratorMetadata($mapperMetadata->source, $mapperMetadata->target) + ))); } public function buildMappers(MetadataRegistry $registry): bool diff --git a/src/Loader/FileLoader.php b/src/Loader/FileLoader.php index b4986c9..331f19a 100644 --- a/src/Loader/FileLoader.php +++ b/src/Loader/FileLoader.php @@ -88,8 +88,9 @@ public function createGeneratedMapper(MapperMetadata $mapperMetadata): string $className = $mapperMetadata->className; $classPath = $this->directory . \DIRECTORY_SEPARATOR . $className . '.php'; - $generatorMetadata = $this->metadataFactory->getGeneratorMetadata($mapperMetadata->source, $mapperMetadata->target); - $classCode = $this->printer->prettyPrint([$this->generator->generate($generatorMetadata)]); + $classCode = $this->printer->prettyPrint($this->generator->generate( + $this->metadataFactory->getGeneratorMetadata($mapperMetadata->source, $mapperMetadata->target) + )); $this->write($classPath, "variableRegistry = new VariableRegistry(); diff --git a/src/Metadata/MetadataFactory.php b/src/Metadata/MetadataFactory.php index a3ee845..32606da 100644 --- a/src/Metadata/MetadataFactory.php +++ b/src/Metadata/MetadataFactory.php @@ -312,6 +312,7 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera $mapperEvent->checkAttributes ?? $this->configuration->attributeChecking, $mapperEvent->constructorStrategy ?? $this->configuration->constructorStrategy, $mapperEvent->allowReadOnlyTargetToPopulate ?? $this->configuration->allowReadOnlyTargetToPopulate, + $mapperEvent->strictTypes ?? $this->configuration->strictTypes, $mapperEvent->provider, ); } diff --git a/src/php-parser.php b/src/php-parser.php index efb355a..cdd9c93 100644 --- a/src/php-parser.php +++ b/src/php-parser.php @@ -43,3 +43,19 @@ function create_expr_array_item(Expr $value, Expr $key = null, bool $byRef = fal return new $class($value, $key, $byRef, $attributes, $unpack); } + +/** + * Constructs a declare key=>value pair node. + * + * @param string|Node\Identifier $key Key + * @param Expr $value Value + * @param array $attributes Additional attributes + * + * @internal + */ +function create_declare_item($key, Expr $value, array $attributes = []): Node\DeclareItem|Node\Stmt\DeclareDeclare +{ + $class = class_exists(Node\DeclareItem::class) ? Node\DeclareItem::class : Node\Stmt\DeclareDeclare::class; + + return new $class($key, $value, $attributes); +} diff --git a/tests/AutoMapperTest.php b/tests/AutoMapperTest.php index 33418c1..565cb8d 100644 --- a/tests/AutoMapperTest.php +++ b/tests/AutoMapperTest.php @@ -785,6 +785,24 @@ public function testNoAutoRegister(): void $automapper->getMapper(Fixtures\User::class, Fixtures\UserDTO::class); } + public function testStrictTypes(): void + { + $this->expectException(\TypeError::class); + + $automapper = AutoMapper::create(new Configuration(strictTypes: true, classPrefix: 'StrictTypes_')); + $data = ['foo' => 1.1]; + $automapper->map($data, Fixtures\IntDTO::class); + } + + public function testStrictTypesFromMapper(): void + { + $this->expectException(\TypeError::class); + + $automapper = AutoMapper::create(new Configuration(strictTypes: false, classPrefix: 'StrictTypesFromMapper_')); + $data = ['foo' => 1.1]; + $automapper->map($data, Fixtures\IntDTOWithMapper::class); + } + public function testWithMixedArray(): void { $user = new Fixtures\User(1, 'yolo', '13'); diff --git a/tests/Fixtures/IntDTO.php b/tests/Fixtures/IntDTO.php new file mode 100644 index 0000000..c4d37ae --- /dev/null +++ b/tests/Fixtures/IntDTO.php @@ -0,0 +1,13 @@ +