Skip to content
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
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,24 @@ 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)).

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`.

<a name="complete_example"></a>
### Hydrating nested DTOs

Expand Down
4 changes: 2 additions & 2 deletions src/FlatMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
16 changes: 14 additions & 2 deletions src/Mapping/NameTransformation.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,21 @@
#[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 Use $columnPrefix instead */
string $removePrefix = '',
/** @deprecated Use $snakeCaseColumns instead */
bool $camelize = false
) {
$this->columnPrefix = $columnPrefix ?: $removePrefix;
$this->snakeCaseColumns = $snakeCaseColumns ?: $camelize;
}
}
2 changes: 1 addition & 1 deletion src/PixelshapedFlatMapperBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);

namespace Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\NameTransformation;

use Pixelshaped\FlatMapperBundle\Mapping\Identifier;
use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation;

#[Identifier('id')]
// @phpstan-ignore argument.unknown
#[NameTransformation(invalidParameter: 'test')]
final readonly class InvalidNameTransformationDTO
{
public function __construct(
public int $id,
public string $name
) {
}
}
2 changes: 1 addition & 1 deletion tests/Examples/Valid/ClassAttributes/AuthorDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation;
use Pixelshaped\FlatMapperBundle\Mapping\ReferenceArray;

#[NameTransformation(removePrefix: 'author_')]
#[NameTransformation(columnPrefix: 'author_')]
class AuthorDTO
{
/**
Expand Down
2 changes: 1 addition & 1 deletion tests/Examples/Valid/ClassAttributes/BookDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
23 changes: 23 additions & 0 deletions tests/Examples/Valid/NameTransformation/AccountDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);

namespace Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NameTransformation;

use Pixelshaped\FlatMapperBundle\Mapping\Identifier;
use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation;
use Pixelshaped\FlatMapperBundle\Mapping\Scalar;

// Test that Scalar attribute takes precedence over NameTransformation
#[NameTransformation(columnPrefix: 'acc_')]
final readonly class AccountDTO
{
public function __construct(
#[Identifier]
public int $id,
public string $name,
// This should use 'account_balance' not 'acc_balance' because Scalar takes precedence
#[Scalar('account_balance')]
public float $balance
) {
}
}
21 changes: 21 additions & 0 deletions tests/Examples/Valid/NameTransformation/CarDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);

namespace Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NameTransformation;

use Pixelshaped\FlatMapperBundle\Mapping\Identifier;
use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation;

// Test that Identifier attribute takes precedence over NameTransformation
#[NameTransformation(columnPrefix: 'car_', snakeCaseColumns: true)]
final readonly class CarDTO
{
public function __construct(
// Identifier with explicit column name should bypass transformation
#[Identifier('vehicle_id')]
public int $VehicleId,
public string $Model,
public string $Brand
) {
}
}
19 changes: 19 additions & 0 deletions tests/Examples/Valid/NameTransformation/ItemDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);

namespace Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NameTransformation;

use Pixelshaped\FlatMapperBundle\Mapping\Identifier;
use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation;

#[NameTransformation(snakeCaseColumns: true)]
final readonly class ItemDTO
{
public function __construct(
#[Identifier]
public int $itemId,
public string $itemName,
public float $itemPrice
) {
}
}
18 changes: 18 additions & 0 deletions tests/Examples/Valid/NameTransformation/LegacyDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);

namespace Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NameTransformation;

use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation;

// Test backward compatibility with old parameter names
#[NameTransformation(removePrefix: 'legacy_', camelize: true)]
final readonly class LegacyDTO
{
public function __construct(
#[\Pixelshaped\FlatMapperBundle\Mapping\Identifier]
public int $Id,
public string $Name
) {
}
}
19 changes: 19 additions & 0 deletions tests/Examples/Valid/NameTransformation/OrderDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);

namespace Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NameTransformation;

use Pixelshaped\FlatMapperBundle\Mapping\Identifier;
use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation;

#[NameTransformation(columnPrefix: 'order_', snakeCaseColumns: true)]
final readonly class OrderDTO
{
public function __construct(
#[Identifier]
public int $Id,
public string $CustomerName,
public float $TotalAmount
) {
}
}
19 changes: 19 additions & 0 deletions tests/Examples/Valid/NameTransformation/PersonDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);

namespace Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NameTransformation;

use Pixelshaped\FlatMapperBundle\Mapping\Identifier;
use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation;

#[Identifier('person_id')]
#[NameTransformation(columnPrefix: 'person_')]
final readonly class PersonDTO
{
public function __construct(
public int $id,
public string $name,
public int $age
) {
}
}
19 changes: 19 additions & 0 deletions tests/Examples/Valid/NameTransformation/ProductDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);

namespace Pixelshaped\FlatMapperBundle\Tests\Examples\Valid\NameTransformation;

use Pixelshaped\FlatMapperBundle\Mapping\Identifier;
use Pixelshaped\FlatMapperBundle\Mapping\NameTransformation;

#[NameTransformation(snakeCaseColumns: true)]
final readonly class ProductDTO
{
public function __construct(
#[Identifier]
public int $ProductId,
public string $ProductName,
public float $ProductPrice
) {
}
}
23 changes: 23 additions & 0 deletions tests/FlatMapperCreateMappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPUnit\Framework\TestCase;
use Pixelshaped\FlatMapperBundle\Exception\MappingCreationException;
use Pixelshaped\FlatMapperBundle\FlatMapper;
use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\NameTransformation\InvalidNameTransformationDTO;
use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTO as InvalidRootDTO;
use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTOWithEmptyClassIdentifier;
use Pixelshaped\FlatMapperBundle\Tests\Examples\Invalid\RootDTOWithNoIdentifier;
Expand Down Expand Up @@ -49,6 +50,21 @@ public function testCreateMappingWithCacheServiceDoesNotAssert(): void
$flatMapper->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);
Expand Down Expand Up @@ -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);
}
}
Loading