Skip to content

Commit 00b9db7

Browse files
committed
feature #58060 [Serializer] Add SnakeCaseToCamelCaseNameConverter (dunglas)
This PR was squashed before being merged into the 7.2 branch. Discussion ---------- [Serializer] Add SnakeCaseToCamelCaseNameConverter | Q | A | ------------- | --- | Branch? | 7.2 | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Issues | n/a | License | MIT Using snake_cased properties in PHP classes is a popular convention. For instance, it is used by Eloquent (the Laravel ORM). This new name converter makes it easier to use the Symfony Serializer with class following this convention. This name converter mirrors the existing `CamelCaseToSnakeCaseNameConverter` behavior and uses the same dataset in tests. Commits ------- b8ec3d9 [Serializer] Add SnakeCaseToCamelCaseNameConverter
2 parents 1dbc826 + b8ec3d9 commit 00b9db7

File tree

6 files changed

+153
-1
lines changed

6 files changed

+153
-1
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* [BC BREAK] The `secrets:decrypt-to-local` command terminates with a non-zero exit code when a secret could not be read
1212
* Deprecate making `cache.app` adapter taggable, use the `cache.app.taggable` adapter instead
1313
* Enable `json_decode_detailed_errors` in the default serializer context in debug mode by default when `seld/jsonlint` is installed
14+
* Register `Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter` as a service named `serializer.name_converter.snake_case_to_camel_case` if available
1415

1516
7.1
1617
---

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

+6
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
159159
use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader;
160160
use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader;
161+
use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter;
161162
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
162163
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
163164
use Symfony\Component\Serializer\Serializer;
@@ -1849,6 +1850,11 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder
18491850
$container->removeDefinition('serializer.normalizer.mime_message');
18501851
}
18511852

1853+
// BC layer Serializer < 7.2
1854+
if (!class_exists(SnakeCaseToCamelCaseNameConverter::class)) {
1855+
$container->removeDefinition('serializer.name_converter.snake_case_to_camel_case');
1856+
}
1857+
18521858
if ($container->getParameter('kernel.debug')) {
18531859
$container->removeDefinition('serializer.mapping.cache_class_metadata_factory');
18541860
}

src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use Symfony\Component\Serializer\Mapping\Loader\LoaderChain;
3232
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
3333
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
34+
use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter;
3435
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
3536
use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer;
3637
use Symfony\Component\Serializer\Normalizer\ConstraintViolationListNormalizer;
@@ -186,8 +187,9 @@
186187
->set('serializer.encoder.csv', CsvEncoder::class)
187188
->tag('serializer.encoder')
188189

189-
// Name converter
190+
// Name converters
190191
->set('serializer.name_converter.camel_case_to_snake_case', CamelCaseToSnakeCaseNameConverter::class)
192+
->set('serializer.name_converter.snake_case_to_camel_case', SnakeCaseToCamelCaseNameConverter::class)
191193

192194
->set('serializer.name_converter.metadata_aware', MetadataAwareNameConverter::class)
193195
->args([service('serializer.mapping.class_metadata_factory')])

src/Symfony/Component/Serializer/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Deprecate the `csv_escape_char` context option of `CsvEncoder` and the `CsvEncoder::ESCAPE_CHAR_KEY` constant
88
* Deprecate `CsvEncoderContextBuilder::withEscapeChar()` method
9+
* Add `SnakeCaseToCamelCaseNameConverter`
910

1011
7.1
1112
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\NameConverter;
13+
14+
use Symfony\Component\Serializer\Exception\UnexpectedPropertyException;
15+
16+
/**
17+
* Underscore to camelCase name converter.
18+
*
19+
* @author Kévin Dunglas <kevin@dunglas.dev>
20+
*/
21+
final readonly class SnakeCaseToCamelCaseNameConverter implements NameConverterInterface
22+
{
23+
/**
24+
* Require all properties to be written in camelCase.
25+
*/
26+
public const REQUIRE_CAMEL_CASE_PROPERTIES = 'require_camel_case_properties';
27+
28+
/**
29+
* @param string[]|null $attributes The list of attributes to rename or null for all attributes
30+
* @param bool $lowerCamelCase Use lowerCamelCase style
31+
*/
32+
public function __construct(
33+
private ?array $attributes = null,
34+
private bool $lowerCamelCase = true,
35+
) {
36+
}
37+
38+
/**
39+
* @param class-string|null $class
40+
* @param array<string, mixed> $context
41+
*/
42+
public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string
43+
{
44+
if (null !== $this->attributes && !\in_array($propertyName, $this->attributes, true)) {
45+
return $propertyName;
46+
}
47+
48+
$camelCasedName = preg_replace_callback(
49+
'/(^|_|\.)+(.)/',
50+
fn ($match) => ('.' === $match[1] ? '_' : '').strtoupper($match[2]),
51+
$propertyName
52+
);
53+
54+
if ($this->lowerCamelCase) {
55+
$camelCasedName = lcfirst($camelCasedName);
56+
}
57+
58+
return $camelCasedName;
59+
}
60+
61+
/**
62+
* @param class-string|null $class
63+
* @param array<string, mixed> $context
64+
*/
65+
public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string
66+
{
67+
if (($context[self::REQUIRE_CAMEL_CASE_PROPERTIES] ?? false) && $propertyName !== $this->normalize($propertyName, $class, $format, $context)) {
68+
throw new UnexpectedPropertyException($propertyName);
69+
}
70+
71+
$snakeCased = strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($propertyName)));
72+
if (null === $this->attributes || \in_array($snakeCased, $this->attributes, true)) {
73+
return $snakeCased;
74+
}
75+
76+
return $propertyName;
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Serializer\Tests\NameConverter;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Serializer\Exception\UnexpectedPropertyException;
16+
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
17+
use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter;
18+
19+
/**
20+
* @author Kévin Dunglas <dunglas@gmail.com>
21+
* @author Aurélien Pillevesse <aurelienpillevesse@hotmail.fr>
22+
*/
23+
class SnakeCaseToCamelCaseNameConverterTest extends TestCase
24+
{
25+
public function testInterface()
26+
{
27+
$attributeMetadata = new SnakeCaseToCamelCaseNameConverter();
28+
$this->assertInstanceOf(NameConverterInterface::class, $attributeMetadata);
29+
}
30+
31+
/**
32+
* @dataProvider Symfony\Component\Serializer\Tests\NameConverter\CamelCaseToSnakeCaseNameConverterTest::attributeProvider
33+
*/
34+
public function testNormalize($underscored, $camelCased, $useLowerCamelCase)
35+
{
36+
$nameConverter = new SnakeCaseToCamelCaseNameConverter(null, $useLowerCamelCase);
37+
$this->assertEquals($camelCased, $nameConverter->normalize($underscored));
38+
}
39+
40+
/**
41+
* @dataProvider Symfony\Component\Serializer\Tests\NameConverter\CamelCaseToSnakeCaseNameConverterTest::attributeProvider
42+
*/
43+
public function testDenormalize($underscored, $camelCased, $useLowerCamelCase)
44+
{
45+
$nameConverter = new SnakeCaseToCamelCaseNameConverter(null, $useLowerCamelCase);
46+
$this->assertEquals($underscored, $nameConverter->denormalize($camelCased));
47+
}
48+
49+
public function testDenormalizeWithContext()
50+
{
51+
$nameConverter = new SnakeCaseToCamelCaseNameConverter(null, true);
52+
$denormalizedValue = $nameConverter->denormalize('lastName', null, null, [SnakeCaseToCamelCaseNameConverter::REQUIRE_CAMEL_CASE_PROPERTIES => true]);
53+
54+
$this->assertSame('last_name', $denormalizedValue);
55+
}
56+
57+
public function testErrorDenormalizeWithContext()
58+
{
59+
$nameConverter = new SnakeCaseToCamelCaseNameConverter(null, true);
60+
61+
$this->expectException(UnexpectedPropertyException::class);
62+
$nameConverter->denormalize('last_name', null, null, [SnakeCaseToCamelCaseNameConverter::REQUIRE_CAMEL_CASE_PROPERTIES => true]);
63+
}
64+
}

0 commit comments

Comments
 (0)