From 87f80b9456c07e90325af3579c5f1c1740a75e1a Mon Sep 17 00:00:00 2001 From: Renaud Date: Fri, 26 Dec 2025 11:48:34 +0100 Subject: [PATCH 01/10] New 100% baseline --- .../InvalidNameTransformationDTO.php | 18 +++++++++ .../Valid/NameTransformation/PersonDTO.php | 19 +++++++++ .../Valid/NameTransformation/ProductDTO.php | 19 +++++++++ tests/FlatMapperCreateMappingTest.php | 23 +++++++++++ tests/FlatMapperTest.php | 40 +++++++++++++++++++ 5 files changed, 119 insertions(+) create mode 100644 tests/Examples/Invalid/NameTransformation/InvalidNameTransformationDTO.php create mode 100644 tests/Examples/Valid/NameTransformation/PersonDTO.php create mode 100644 tests/Examples/Valid/NameTransformation/ProductDTO.php diff --git a/tests/Examples/Invalid/NameTransformation/InvalidNameTransformationDTO.php b/tests/Examples/Invalid/NameTransformation/InvalidNameTransformationDTO.php new file mode 100644 index 0000000..5fd3bf9 --- /dev/null +++ b/tests/Examples/Invalid/NameTransformation/InvalidNameTransformationDTO.php @@ -0,0 +1,18 @@ +createMapping(AuthorDTO::class); } + public function testCreateMappingWithCacheServiceMissExecutesCallback(): void + { + $flatMapper = new FlatMapper(); + + $cacheInterface = $this->createMock(CacheInterface::class); + $cacheInterface->expects($this->once())->method('get')->willReturnCallback( + function (string $key, callable $callback) { + return $callback(); + } + ); + + $flatMapper->setCacheService($cacheInterface); + $flatMapper->createMapping(AuthorDTO::class); + } + public function testCreateMappingWrongClassNameAsserts(): void { $this->expectException(MappingCreationException::class); @@ -103,4 +119,11 @@ public function testCreateMappingWithEmptyClassIdentifierAsserts(): void $this->expectExceptionMessageMatches("/The Identifier attribute cannot be used without a property name when used as a Class attribute/"); (new FlatMapper())->createMapping(RootDTOWithEmptyClassIdentifier::class); } + + public function testCreateMappingWithInvalidNameTransformationAsserts(): void + { + $this->expectException(MappingCreationException::class); + $this->expectExceptionMessageMatches("/Invalid NameTransformation attribute/"); + (new FlatMapper())->createMapping(InvalidNameTransformationDTO::class); + } } diff --git a/tests/FlatMapperTest.php b/tests/FlatMapperTest.php index df65d13..9d61535 100644 --- a/tests/FlatMapperTest.php +++ b/tests/FlatMapperTest.php @@ -12,6 +12,8 @@ use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\Complex\CustomerDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\Complex\InvoiceDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\Complex\ProductDTO; +use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NameTransformation\PersonDTO; +use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NameTransformation\ProductDTO as NameTransformationProductDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\ReferenceArray\AuthorDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\ReferenceArray\BookDTO; use Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\ScalarArray\ScalarArrayDTO; @@ -240,6 +242,44 @@ public function testMapNestedDTOsWithClassAttributes(): void ); } + public function testMapWithNameTransformationRemovePrefix(): void + { + $results = [ + ['person_id' => 1, 'person_name' => 'John Doe', 'person_age' => 30], + ['person_id' => 2, 'person_name' => 'Jane Smith', 'person_age' => 25], + ]; + + $flatMapperResults = ((new FlatMapper())->map(PersonDTO::class, $results)); + + $personDto1 = new PersonDTO(1, "John Doe", 30); + $personDto2 = new PersonDTO(2, "Jane Smith", 25); + $handmadeResult = [1 => $personDto1, 2 => $personDto2]; + + $this->assertSame( + var_export($flatMapperResults, true), + var_export($handmadeResult, true) + ); + } + + public function testMapWithNameTransformationCamelize(): void + { + $results = [ + ['product_id' => 1, 'product_name' => 'Widget', 'product_price' => 19.99], + ['product_id' => 2, 'product_name' => 'Gadget', 'product_price' => 29.99], + ]; + + $flatMapperResults = ((new FlatMapper())->map(NameTransformationProductDTO::class, $results)); + + $productDto1 = new NameTransformationProductDTO(1, "Widget", 19.99); + $productDto2 = new NameTransformationProductDTO(2, "Gadget", 29.99); + $handmadeResult = [1 => $productDto1, 2 => $productDto2]; + + $this->assertSame( + var_export($flatMapperResults, true), + var_export($handmadeResult, true) + ); + } + /** * @return list> */ From ce6dd158e5dfc75c05c3a742598f8741a57d6774 Mon Sep 17 00:00:00 2001 From: Renaud Date: Fri, 26 Dec 2025 12:32:02 +0100 Subject: [PATCH 02/10] Add more tests --- .../Valid/NameTransformation/OrderDTO.php | 19 ++++++ tests/FlatMapperTest.php | 60 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 tests/Examples/Valid/NameTransformation/OrderDTO.php diff --git a/tests/Examples/Valid/NameTransformation/OrderDTO.php b/tests/Examples/Valid/NameTransformation/OrderDTO.php new file mode 100644 index 0000000..37ecf58 --- /dev/null +++ b/tests/Examples/Valid/NameTransformation/OrderDTO.php @@ -0,0 +1,19 @@ +expectException(MappingException::class); + $this->expectExceptionMessageMatches('/Data does not contain required property: person_name/'); + + $results = [ + ['person_id' => 1, 'name' => 'John Doe', 'age' => 30], // Missing person_ prefix + ['person_id' => 2, 'name' => 'Jane Smith', 'age' => 25], + ]; + + ((new FlatMapper())->map(PersonDTO::class, $results)); + } + public function testMapWithNameTransformationCamelize(): void { $results = [ @@ -280,6 +294,52 @@ public function testMapWithNameTransformationCamelize(): void ); } + public function testMapWithNameTransformationCamelizeWhenDatasetUsesWrongCase(): void + { + $this->expectException(MappingException::class); + $this->expectExceptionMessageMatches('/Data does not contain required property: product_name/'); + + $results = [ + ['product_id' => 1, 'ProductName' => 'Widget', 'ProductPrice' => 19.99], // Wrong case + ['product_id' => 2, 'ProductName' => 'Gadget', 'ProductPrice' => 29.99], + ]; + + ((new FlatMapper())->map(NameTransformationProductDTO::class, $results)); + } + + public function testMapWithNameTransformationBothPrefixAndCamelize(): void + { + $results = [ + ['order_id' => 1, 'order_customer_name' => 'John Doe', 'order_total_amount' => 99.99], + ['order_id' => 2, 'order_customer_name' => 'Jane Smith', 'order_total_amount' => 149.99], + ]; + + $flatMapperResults = ((new FlatMapper())->map(OrderDTO::class, $results)); + + $orderDto1 = new OrderDTO(1, "John Doe", 99.99); + $orderDto2 = new OrderDTO(2, "Jane Smith", 149.99); + $handmadeResult = [1 => $orderDto1, 2 => $orderDto2]; + + $this->assertSame( + var_export($flatMapperResults, true), + var_export($handmadeResult, true) + ); + } + + public function testMapWithNameTransformationBothPrefixAndCamelizeWhenDatasetIncorrect(): void + { + $this->expectException(MappingException::class); + $this->expectExceptionMessageMatches('/Data does not contain required property: order_customer_name/'); + + $results = [ + // Missing order_ prefix - just using snake_case + ['order_id' => 1, 'customer_name' => 'John Doe', 'total_amount' => 99.99], + ['order_id' => 2, 'customer_name' => 'Jane Smith', 'total_amount' => 149.99], + ]; + + ((new FlatMapper())->map(OrderDTO::class, $results)); + } + /** * @return list> */ From db5e95813c229264b979f0eccbca8c14a2d831db Mon Sep 17 00:00:00 2001 From: Renaud Date: Fri, 26 Dec 2025 12:47:30 +0100 Subject: [PATCH 03/10] Add camelCase test object --- .../Valid/NameTransformation/ItemDTO.php | 19 ++++++++++++++ .../Valid/NameTransformation/ProductDTO.php | 2 +- tests/FlatMapperTest.php | 26 ++++++++++++++++--- 3 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 tests/Examples/Valid/NameTransformation/ItemDTO.php diff --git a/tests/Examples/Valid/NameTransformation/ItemDTO.php b/tests/Examples/Valid/NameTransformation/ItemDTO.php new file mode 100644 index 0000000..09ed31e --- /dev/null +++ b/tests/Examples/Valid/NameTransformation/ItemDTO.php @@ -0,0 +1,19 @@ + 1, 'item_name' => 'Widget', 'item_price' => 19.99], + ['item_id' => 2, 'item_name' => 'Gadget', 'item_price' => 29.99], + ]; + + $flatMapperResults = ((new FlatMapper())->map(ItemDTO::class, $results)); + + $itemDto1 = new ItemDTO(1, "Widget", 19.99); + $itemDto2 = new ItemDTO(2, "Gadget", 29.99); + $handmadeResult = [1 => $itemDto1, 2 => $itemDto2]; + + $this->assertSame( + var_export($flatMapperResults, true), + var_export($handmadeResult, true) + ); + } + + public function testMapWithNameTransformationCamelizeWhenDatasetNotSnakeCase(): void { $this->expectException(MappingException::class); $this->expectExceptionMessageMatches('/Data does not contain required property: product_name/'); $results = [ - ['product_id' => 1, 'ProductName' => 'Widget', 'ProductPrice' => 19.99], // Wrong case + ['product_id' => 1, 'ProductName' => 'Widget', 'ProductPrice' => 19.99], ['product_id' => 2, 'ProductName' => 'Gadget', 'ProductPrice' => 29.99], ]; @@ -332,7 +352,7 @@ public function testMapWithNameTransformationBothPrefixAndCamelizeWhenDatasetInc $this->expectExceptionMessageMatches('/Data does not contain required property: order_customer_name/'); $results = [ - // Missing order_ prefix - just using snake_case + // Missing order_ prefix ['order_id' => 1, 'customer_name' => 'John Doe', 'total_amount' => 99.99], ['order_id' => 2, 'customer_name' => 'Jane Smith', 'total_amount' => 149.99], ]; From 2a3d2fc4c75c7afb734fa75a3208e2de8ec18b6a Mon Sep 17 00:00:00 2001 From: Renaud Date: Fri, 26 Dec 2025 12:55:46 +0100 Subject: [PATCH 04/10] Add test for Identifier precedence --- .../Valid/NameTransformation/CarDTO.php | 21 +++++++++ tests/FlatMapperTest.php | 46 +++++++++++++++++-- 2 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 tests/Examples/Valid/NameTransformation/CarDTO.php diff --git a/tests/Examples/Valid/NameTransformation/CarDTO.php b/tests/Examples/Valid/NameTransformation/CarDTO.php new file mode 100644 index 0000000..817e1ee --- /dev/null +++ b/tests/Examples/Valid/NameTransformation/CarDTO.php @@ -0,0 +1,21 @@ +expectException(MappingException::class); - $this->expectExceptionMessageMatches('/Data does not contain required property: product_name/'); - + $this->expectExceptionMessageMatches('/Identifier not found: product_id/'); + $results = [ - ['product_id' => 1, 'ProductName' => 'Widget', 'ProductPrice' => 19.99], - ['product_id' => 2, 'ProductName' => 'Gadget', 'ProductPrice' => 29.99], + ['ProductId' => 1, 'ProductName' => 'Widget', 'ProductPrice' => 19.99], + ['ProductId' => 2, 'ProductName' => 'Gadget', 'ProductPrice' => 29.99], ]; ((new FlatMapper())->map(NameTransformationProductDTO::class, $results)); @@ -360,6 +361,43 @@ public function testMapWithNameTransformationBothPrefixAndCamelizeWhenDatasetInc ((new FlatMapper())->map(OrderDTO::class, $results)); } + public function testMapWithNameTransformationIdentifierAttributeTakesPrecedence(): void + { + // CarDTO has NameTransformation(removePrefix: 'car_', camelize: true) + // But the Identifier attribute explicitly specifies 'vehicle_id' + // This tests that Identifier attribute takes precedence over NameTransformation + $results = [ + ['vehicle_id' => 1, 'car_model' => 'Civic', 'car_brand' => 'Honda'], + ['vehicle_id' => 2, 'car_model' => 'Corolla', 'car_brand' => 'Toyota'], + ]; + + $flatMapperResults = ((new FlatMapper())->map(CarDTO::class, $results)); + + $carDto1 = new CarDTO(1, "Civic", "Honda"); + $carDto2 = new CarDTO(2, "Corolla", "Toyota"); + $handmadeResult = [1 => $carDto1, 2 => $carDto2]; + + $this->assertSame( + var_export($flatMapperResults, true), + var_export($handmadeResult, true) + ); + } + + public function testMapWithNameTransformationIdentifierPrecedenceWhenDatasetWrong(): void + { + $this->expectException(MappingException::class); + $this->expectExceptionMessageMatches('/Identifier not found: vehicle_id/'); + + // This should fail because the Identifier attribute expects 'vehicle_id' + // not 'car_vehicle_id' (which would be the result of applying the transformation) + $results = [ + ['car_vehicle_id' => 1, 'car_model' => 'Civic', 'car_brand' => 'Honda'], + ['car_vehicle_id' => 2, 'car_model' => 'Corolla', 'car_brand' => 'Toyota'], + ]; + + ((new FlatMapper())->map(CarDTO::class, $results)); + } + /** * @return list> */ From e6abc121e8c9db27eeff5d7407b996303a14dbde Mon Sep 17 00:00:00 2001 From: Renaud Date: Fri, 26 Dec 2025 13:01:08 +0100 Subject: [PATCH 05/10] Add test for Scalar precedence --- .../Valid/NameTransformation/AccountDTO.php | 23 +++++++++++ tests/FlatMapperTest.php | 38 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/Examples/Valid/NameTransformation/AccountDTO.php diff --git a/tests/Examples/Valid/NameTransformation/AccountDTO.php b/tests/Examples/Valid/NameTransformation/AccountDTO.php new file mode 100644 index 0000000..a7d52e0 --- /dev/null +++ b/tests/Examples/Valid/NameTransformation/AccountDTO.php @@ -0,0 +1,23 @@ +map(CarDTO::class, $results)); } + public function testMapWithNameTransformationScalarAttributeTakesPrecedence(): void + { + // AccountDTO has NameTransformation(removePrefix: 'acc_') + // But the $balance property has #[Scalar('account_balance')] + // This tests that Scalar attribute takes precedence over NameTransformation + $results = [ + ['acc_id' => 1, 'acc_name' => 'Savings', 'account_balance' => 1000.50], + ['acc_id' => 2, 'acc_name' => 'Checking', 'account_balance' => 500.25], + ]; + + $flatMapperResults = ((new FlatMapper())->map(AccountDTO::class, $results)); + + $accountDto1 = new AccountDTO(1, "Savings", 1000.50); + $accountDto2 = new AccountDTO(2, "Checking", 500.25); + $handmadeResult = [1 => $accountDto1, 2 => $accountDto2]; + + $this->assertSame( + var_export($flatMapperResults, true), + var_export($handmadeResult, true) + ); + } + + public function testMapWithNameTransformationScalarPrecedenceWhenDatasetWrong(): void + { + $this->expectException(MappingException::class); + $this->expectExceptionMessageMatches('/Data does not contain required property: account_balance/'); + + // This should fail because the Scalar attribute expects 'account_balance' + // not 'acc_balance' (which would be the result of applying the transformation) + $results = [ + ['acc_id' => 1, 'acc_name' => 'Savings', 'acc_balance' => 1000.50], + ['acc_id' => 2, 'acc_name' => 'Checking', 'acc_balance' => 500.25], + ]; + + ((new FlatMapper())->map(AccountDTO::class, $results)); + } + /** * @return list> */ From a370fb474c2856336bf269e34ac96eb0292ef1c9 Mon Sep 17 00:00:00 2001 From: Renaud Date: Fri, 26 Dec 2025 13:04:59 +0100 Subject: [PATCH 06/10] Remove redundant test --- .../InvalidNameTransformationDTO.php | 18 ------------------ tests/FlatMapperCreateMappingTest.php | 7 ------- 2 files changed, 25 deletions(-) delete mode 100644 tests/Examples/Invalid/NameTransformation/InvalidNameTransformationDTO.php diff --git a/tests/Examples/Invalid/NameTransformation/InvalidNameTransformationDTO.php b/tests/Examples/Invalid/NameTransformation/InvalidNameTransformationDTO.php deleted file mode 100644 index 5fd3bf9..0000000 --- a/tests/Examples/Invalid/NameTransformation/InvalidNameTransformationDTO.php +++ /dev/null @@ -1,18 +0,0 @@ -expectExceptionMessageMatches("/The Identifier attribute cannot be used without a property name when used as a Class attribute/"); (new FlatMapper())->createMapping(RootDTOWithEmptyClassIdentifier::class); } - - public function testCreateMappingWithInvalidNameTransformationAsserts(): void - { - $this->expectException(MappingCreationException::class); - $this->expectExceptionMessageMatches("/Invalid NameTransformation attribute/"); - (new FlatMapper())->createMapping(InvalidNameTransformationDTO::class); - } } From eadd51a6f6d373da64ecc26b8c5427e3a8ba3855 Mon Sep 17 00:00:00 2001 From: Renaud Date: Fri, 26 Dec 2025 13:23:57 +0100 Subject: [PATCH 07/10] Make camelize and removePrefix properties deprecated. Adopt clearer naming. --- README.md | 13 ++++++----- src/FlatMapper.php | 4 ++-- src/Mapping/NameTransformation.php | 17 ++++++++++++-- .../Valid/ClassAttributes/AuthorDTO.php | 2 +- .../Valid/ClassAttributes/BookDTO.php | 2 +- .../Valid/NameTransformation/AccountDTO.php | 2 +- .../Valid/NameTransformation/CarDTO.php | 2 +- .../Valid/NameTransformation/ItemDTO.php | 2 +- .../Valid/NameTransformation/LegacyDTO.php | 18 +++++++++++++++ .../Valid/NameTransformation/OrderDTO.php | 2 +- .../Valid/NameTransformation/PersonDTO.php | 2 +- .../Valid/NameTransformation/ProductDTO.php | 2 +- tests/FlatMapperTest.php | 22 +++++++++++++++++++ 13 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 tests/Examples/Valid/NameTransformation/LegacyDTO.php diff --git a/README.md b/README.md index 2eaaf06..227896b 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ This bundle comes with several attributes that you can use to add mapping to you - Use it as a Class attribute if you don't intend to use the property yourself ([see example](tests/Examples/Valid/Complex/ProductDTO.php)). It will then only be used internally and not be mapped to your DTO. - Use it as a Property attribute if you have some use for it ([see example](tests/Examples/Valid/Complex/CustomerDTO.php)). - Specify the mapped property name directly on the attribute ([see example](tests/Examples/Valid/Complex/InvoiceDTO.php)). This is mandatory when used as a Class attribute. - - Specify the mapped property name separately with the `InboundProperty` attribute, Doctrine-style ([see example](tests/Examples/Valid/ReferenceArray/AuthorDTO.php)). + - Specify the mapped property name separately with the `#[Scalar]` attribute ([see example](tests/Examples/Valid/ReferenceArray/AuthorDTO.php)). - `#[Scalar("mapped_property_name")]`: The column `mapped_property_name` of your result set will be mapped to a scalar property of your DTO (the value of the first row will be used). This is optional if your DTO's property names are already matching the result set ([see example](tests/Examples/Valid/WithoutAttributeDTO.php)). - `#[ReferenceArray(NestedDTO::class)]`: An array of `NestedDTO` will be created using the mapping information contained in `NestedDTO`. - `#[ScalarArray("mapped_property_name")]` The column `mapped_property_name` of your result set will be mapped as an array of scalar properties, such as IDs ([see example](tests/Examples/Valid/ScalarArray/ScalarArrayDTO.php)). @@ -85,13 +85,16 @@ This bundle comes with several attributes that you can use to add mapping to you If the mapping between result columns and DTO properties is consistent, you can use the `#[NameTransformation]` class attribute instead of adding `#[Scalar(...)]` to each property: -- `#[NameTransformation(removePrefix: 'foo_')]`: the columns named `foo_bar` and `foo_baz` will be mapped to - `$bar` and `$baz` properties of a DTO class. -- `#[NameTransformation(camelize: true)]`: the column with snake-case name `foo_bar` will be mapped to `$fooBar` property. -- If both of the above rules are enabled, then `foo_bar_baz` result column will be mapped to `$barBaz` property. +- `#[NameTransformation(columnPrefix: 'foo_')]`: adds the prefix `foo_` to property names when looking up columns. + For example, properties `$bar` and `$baz` will look for columns `foo_bar` and `foo_baz`. +- `#[NameTransformation(snakeCaseColumns: true)]`: converts camelCase/PascalCase property names to snake_case when looking up columns. + For example, property `$fooBar` or `$FooBar` will look for column `foo_bar`. +- If both of the above rules are enabled, then property `$barBaz` will look for column `foo_bar_baz`. - Adding a `#[Scalar]` attribute or an `#[Identifier]` attribute with explicitly given name to a property will override mapping set up on class level. +**Note:** The old parameter names `removePrefix` and `camelize` are still supported for backward compatibility but are deprecated in favor of `columnPrefix` and `snakeCaseColumns`. + ### Hydrating nested DTOs diff --git a/src/FlatMapper.php b/src/FlatMapper.php index ae2440d..79066bf 100644 --- a/src/FlatMapper.php +++ b/src/FlatMapper.php @@ -165,14 +165,14 @@ private function createMappingRecursive(string $dtoClassName, ?array& $objectIde private function transformPropertyName(string $propertyName, NameTransformation $transformation): string { - if ($transformation->camelize) { + if ($transformation->snakeCaseColumns) { $propertyName = strtolower(preg_replace( ['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $propertyName ) ?? $propertyName); } - return $transformation->removePrefix . $propertyName; + return $transformation->columnPrefix . $propertyName; } /** diff --git a/src/Mapping/NameTransformation.php b/src/Mapping/NameTransformation.php index 2d8bab3..49aba81 100644 --- a/src/Mapping/NameTransformation.php +++ b/src/Mapping/NameTransformation.php @@ -8,9 +8,22 @@ #[Attribute(Attribute::TARGET_CLASS)] final readonly class NameTransformation { + public readonly string $columnPrefix; + public readonly bool $snakeCaseColumns; + public function __construct( - public string $removePrefix = '', - public bool $camelize = false + // New parameter names (recommended) + string $columnPrefix = '', + bool $snakeCaseColumns = false, + + // Old parameter names (deprecated but still supported for backward compatibility) + /** @deprecated Use $columnPrefix instead */ + string $removePrefix = '', + /** @deprecated Use $snakeCaseColumns instead */ + bool $camelize = false ) { + // Use new names if provided, otherwise fall back to old names for backward compatibility + $this->columnPrefix = $columnPrefix ?: $removePrefix; + $this->snakeCaseColumns = $snakeCaseColumns ?: $camelize; } } diff --git a/tests/Examples/Valid/ClassAttributes/AuthorDTO.php b/tests/Examples/Valid/ClassAttributes/AuthorDTO.php index 624019e..53a55a5 100644 --- a/tests/Examples/Valid/ClassAttributes/AuthorDTO.php +++ b/tests/Examples/Valid/ClassAttributes/AuthorDTO.php @@ -7,7 +7,7 @@ use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation; use Pixelshaped\FlatMapperBundle\Mapping\ReferenceArray; -#[NameTransformation(removePrefix: 'author_')] +#[NameTransformation(columnPrefix: 'author_')] class AuthorDTO { /** diff --git a/tests/Examples/Valid/ClassAttributes/BookDTO.php b/tests/Examples/Valid/ClassAttributes/BookDTO.php index ae2feb5..5d369fa 100644 --- a/tests/Examples/Valid/ClassAttributes/BookDTO.php +++ b/tests/Examples/Valid/ClassAttributes/BookDTO.php @@ -6,7 +6,7 @@ use Pixelshaped\FlatMapperBundle\Mapping\Identifier; use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation; -#[NameTransformation(removePrefix: 'book_', camelize: true)] +#[NameTransformation(columnPrefix: 'book_', snakeCaseColumns: true)] class BookDTO { public function __construct( diff --git a/tests/Examples/Valid/NameTransformation/AccountDTO.php b/tests/Examples/Valid/NameTransformation/AccountDTO.php index a7d52e0..088cc7a 100644 --- a/tests/Examples/Valid/NameTransformation/AccountDTO.php +++ b/tests/Examples/Valid/NameTransformation/AccountDTO.php @@ -8,7 +8,7 @@ use Pixelshaped\FlatMapperBundle\Mapping\Scalar; // Test that Scalar attribute takes precedence over NameTransformation -#[NameTransformation(removePrefix: 'acc_')] +#[NameTransformation(columnPrefix: 'acc_')] final readonly class AccountDTO { public function __construct( diff --git a/tests/Examples/Valid/NameTransformation/CarDTO.php b/tests/Examples/Valid/NameTransformation/CarDTO.php index 817e1ee..c5b2977 100644 --- a/tests/Examples/Valid/NameTransformation/CarDTO.php +++ b/tests/Examples/Valid/NameTransformation/CarDTO.php @@ -7,7 +7,7 @@ use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation; // Test that Identifier attribute takes precedence over NameTransformation -#[NameTransformation(removePrefix: 'car_', camelize: true)] +#[NameTransformation(columnPrefix: 'car_', snakeCaseColumns: true)] final readonly class CarDTO { public function __construct( diff --git a/tests/Examples/Valid/NameTransformation/ItemDTO.php b/tests/Examples/Valid/NameTransformation/ItemDTO.php index 09ed31e..c132b8c 100644 --- a/tests/Examples/Valid/NameTransformation/ItemDTO.php +++ b/tests/Examples/Valid/NameTransformation/ItemDTO.php @@ -6,7 +6,7 @@ use Pixelshaped\FlatMapperBundle\Mapping\Identifier; use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation; -#[NameTransformation(camelize: true)] +#[NameTransformation(snakeCaseColumns: true)] final readonly class ItemDTO { public function __construct( diff --git a/tests/Examples/Valid/NameTransformation/LegacyDTO.php b/tests/Examples/Valid/NameTransformation/LegacyDTO.php new file mode 100644 index 0000000..73ad851 --- /dev/null +++ b/tests/Examples/Valid/NameTransformation/LegacyDTO.php @@ -0,0 +1,18 @@ +map(AccountDTO::class, $results)); } + public function testMapWithNameTransformationBackwardCompatibility(): void + { + // Test that old parameter names (removePrefix, camelize) still work + // LegacyDTO uses the old parameter names + $results = [ + ['legacy_id' => 1, 'legacy_name' => 'Test'], + ['legacy_id' => 2, 'legacy_name' => 'Demo'], + ]; + + $flatMapperResults = ((new FlatMapper())->map(LegacyDTO::class, $results)); + + $legacyDto1 = new LegacyDTO(1, "Test"); + $legacyDto2 = new LegacyDTO(2, "Demo"); + $handmadeResult = [1 => $legacyDto1, 2 => $legacyDto2]; + + $this->assertSame( + var_export($flatMapperResults, true), + var_export($handmadeResult, true) + ); + } + /** * @return list> */ From 9ef89429dd134260ca42827421c2213b72984eb1 Mon Sep 17 00:00:00 2001 From: Renaud Date: Fri, 26 Dec 2025 13:28:00 +0100 Subject: [PATCH 08/10] Add test to improve coverage again --- .../InvalidNameTransformationDTO.php | 19 +++++++++++++++++++ tests/FlatMapperCreateMappingTest.php | 7 +++++++ 2 files changed, 26 insertions(+) create mode 100644 tests/Examples/Invalid/NameTransformation/InvalidNameTransformationDTO.php diff --git a/tests/Examples/Invalid/NameTransformation/InvalidNameTransformationDTO.php b/tests/Examples/Invalid/NameTransformation/InvalidNameTransformationDTO.php new file mode 100644 index 0000000..5922e85 --- /dev/null +++ b/tests/Examples/Invalid/NameTransformation/InvalidNameTransformationDTO.php @@ -0,0 +1,19 @@ +expectExceptionMessageMatches("/The Identifier attribute cannot be used without a property name when used as a Class attribute/"); (new FlatMapper())->createMapping(RootDTOWithEmptyClassIdentifier::class); } + + public function testCreateMappingWithInvalidNameTransformationAsserts(): void + { + $this->expectException(MappingCreationException::class); + $this->expectExceptionMessageMatches("/Invalid NameTransformation attribute/"); + (new FlatMapper())->createMapping(InvalidNameTransformationDTO::class); + } } From fc5eb4063bc1e296ef0ff63963746e6e658f1d95 Mon Sep 17 00:00:00 2001 From: Renaud Date: Fri, 26 Dec 2025 13:31:09 +0100 Subject: [PATCH 09/10] Remove useless comments --- src/Mapping/NameTransformation.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Mapping/NameTransformation.php b/src/Mapping/NameTransformation.php index 49aba81..c68c76b 100644 --- a/src/Mapping/NameTransformation.php +++ b/src/Mapping/NameTransformation.php @@ -16,13 +16,12 @@ public function __construct( string $columnPrefix = '', bool $snakeCaseColumns = false, - // Old parameter names (deprecated but still supported for backward compatibility) + // Old parameter names /** @deprecated Use $columnPrefix instead */ string $removePrefix = '', /** @deprecated Use $snakeCaseColumns instead */ bool $camelize = false ) { - // Use new names if provided, otherwise fall back to old names for backward compatibility $this->columnPrefix = $columnPrefix ?: $removePrefix; $this->snakeCaseColumns = $snakeCaseColumns ?: $camelize; } From eaf4542fc7a23ae537613afe6136790d92865f71 Mon Sep 17 00:00:00 2001 From: Renaud Date: Fri, 26 Dec 2025 13:39:16 +0100 Subject: [PATCH 10/10] Remove phpstan ignore --- src/PixelshapedFlatMapperBundle.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PixelshapedFlatMapperBundle.php b/src/PixelshapedFlatMapperBundle.php index baea2a0..f76e3ce 100644 --- a/src/PixelshapedFlatMapperBundle.php +++ b/src/PixelshapedFlatMapperBundle.php @@ -29,7 +29,7 @@ public function loadExtension(array $config, ContainerConfigurator $container, C public function configure(DefinitionConfigurator $definition): void { - $definition->rootNode() // @phpstan-ignore-line + $definition->rootNode() ->children() ->booleanNode('validate_mapping')->defaultTrue()->end() ->scalarNode('cache_service')->defaultNull()->end()