Skip to content

Commit 0a8286a

Browse files
authored
Merge pull request #15 from sanovskiy/dev
NamingStyle rework
2 parents 91d8119 + 6d3fe86 commit 0a8286a

File tree

3 files changed

+204
-37
lines changed

3 files changed

+204
-37
lines changed

src/NamingConvention.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Sanovskiy\Utility;
4+
enum NamingConvention: string
5+
{
6+
case UPPER_CAMEL = 'UpperCamelCase';
7+
case LOWER_CAMEL = 'lowerCamelCase';
8+
case SNAKE = 'snake_case';
9+
case SCREAMING_SNAKE = 'SCREAMING_SNAKE_CASE';
10+
case KEBAB = 'kebab-case';
11+
case SCREAMING_KEBAB = 'SCREAMING-KEBAB-CASE';
12+
case DOT = 'dot.case';
13+
case TRAIN = 'Train-Case';
14+
}

src/NamingStyle.php

Lines changed: 150 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,73 +4,189 @@
44

55
class NamingStyle
66
{
7+
// === СТАРЫЙ ПУБЛИЧНЫЙ API — НЕ ТРОГАЕМ ===
8+
79
public static function isUpperCamelCase(string $str): bool
810
{
9-
return preg_match('/^[A-Z][a-zA-Z\d]*$/', $str) === 1;
11+
return self::detect($str) === NamingConvention::UPPER_CAMEL;
1012
}
1113

1214
public static function isLowerCamelCase(string $str): bool
1315
{
14-
return preg_match('/^[a-z]*[A-Z][a-zA-Z\d]*$/', $str) === 1;
16+
return self::detect($str) === NamingConvention::LOWER_CAMEL;
1517
}
1618

1719
public static function isSnakeCase(string $str): bool
1820
{
19-
return preg_match('/^[a-z\d]+(_[a-z\d]+)*$/', $str) === 1;
21+
return self::detect($str) === NamingConvention::SNAKE;
2022
}
2123

2224
public static function isScreamingSnakeCase(string $str): bool
2325
{
24-
return preg_match('/^[A-Z\d]+(_[A-Z\d]+)*$/', $str) === 1;
26+
return self::detect($str) === NamingConvention::SCREAMING_SNAKE;
2527
}
2628

2729
public static function toSnakeCase(string $str): string
2830
{
29-
if (!self::isUpperCamelCase($str) && !self::isLowerCamelCase($str)) {
30-
return $str;
31-
}
32-
$str = preg_replace('/(?<!^)([A-Z]|\d+|[A-Z][a-z]+)/', '_$0', $str);
33-
return strtolower($str);
31+
return self::convert($str, NamingConvention::SNAKE);
3432
}
3533

3634
public static function toCamelCase(string $str, bool $capitalizeFirstCharacter = false): string
3735
{
38-
if (!self::isSnakeCase($str) && !self::isScreamingSnakeCase($str)){
39-
return $str;
40-
}
41-
if (self::isScreamingSnakeCase($str)){
42-
$str = strtolower($str);
43-
}
44-
$str = str_replace('_', ' ', $str);
45-
$str = ucwords($str);
46-
$str = str_replace(' ', '', $str);
36+
$target = $capitalizeFirstCharacter
37+
? NamingConvention::UPPER_CAMEL
38+
: NamingConvention::LOWER_CAMEL;
39+
return self::convert($str, $target);
40+
}
4741

48-
if (!$capitalizeFirstCharacter) {
49-
$str = lcfirst($str);
50-
}
42+
public static function isKebabCase(string $str): bool
43+
{
44+
return self::detect($str) === NamingConvention::KEBAB;
45+
}
46+
47+
public static function toKebabCase(string $str): string
48+
{
49+
return self::convert($str, NamingConvention::KEBAB);
50+
}
51+
52+
public static function isDotCase(string $str): bool
53+
{
54+
return self::detect($str) === NamingConvention::DOT;
55+
}
5156

52-
return $str;
57+
public static function isTrainCase(string $str): bool
58+
{
59+
return self::detect($str) === NamingConvention::TRAIN;
5360
}
5461

62+
public static function toDotCase(string $str): string
63+
{
64+
return self::convert($str, NamingConvention::DOT);
65+
}
66+
67+
public static function toTrainCase(string $str): string
68+
{
69+
return self::convert($str, NamingConvention::TRAIN);
70+
}
71+
72+
/**
73+
* @param string $string
74+
* @return string
75+
* @deprecated This method is outdated and will be removed in future versions
76+
*/
5577
public static function swapStyle(string $string): string
5678
{
57-
if (self::isLowerCamelCase($string) || self::isUpperCamelCase($string)){
58-
return self::toSnakeCase($string);
79+
return match (self::detect($string)) {
80+
NamingConvention::UPPER_CAMEL, NamingConvention::LOWER_CAMEL =>
81+
self::convert($string, NamingConvention::SNAKE),
82+
NamingConvention::SNAKE, NamingConvention::SCREAMING_SNAKE, NamingConvention::KEBAB, NamingConvention::SCREAMING_KEBAB =>
83+
self::convert($string, NamingConvention::UPPER_CAMEL),
84+
default => $string,
85+
};
86+
}
87+
88+
public static function getNamingStyle(string $str): string
89+
{
90+
return match (self::detect($str)) {
91+
NamingConvention::UPPER_CAMEL => 'UpperCamelCase',
92+
NamingConvention::LOWER_CAMEL => 'lowerCamelCase',
93+
NamingConvention::SNAKE => 'snake_case',
94+
NamingConvention::SCREAMING_SNAKE => 'SCREAMING_SNAKE_CASE',
95+
NamingConvention::KEBAB => 'kebab-case',
96+
NamingConvention::SCREAMING_KEBAB => 'SCREAMING-KEBAB-CASE',
97+
NamingConvention::DOT => 'dot.case',
98+
NamingConvention::TRAIN => 'Train-Case',
99+
default => 'Unrecognized',
100+
};
101+
}
102+
103+
// === НОВЫЙ ВНУТРЕННИЙ API ===
104+
105+
public static function detect(string $str): ?NamingConvention
106+
{
107+
if (preg_match('/^[A-Z][a-zA-Z\d]*$/', $str) === 1) {
108+
return NamingConvention::UPPER_CAMEL;
109+
}
110+
if (preg_match('/^[a-z]+[A-Z][a-zA-Z\d]*$/', $str) === 1) {
111+
return NamingConvention::LOWER_CAMEL;
112+
}
113+
if (preg_match('/^[a-z\d]+(_[a-z\d]+)*$/', $str) === 1) {
114+
return NamingConvention::SNAKE;
59115
}
60-
if (self::isSnakeCase($string) || self::isScreamingSnakeCase($string)){
61-
return self::toCamelCase($string, true);
116+
if (preg_match('/^[A-Z\d]+(_[A-Z\d]+)*$/', $str) === 1) {
117+
return NamingConvention::SCREAMING_SNAKE;
62118
}
63-
return $string;
119+
if (preg_match('/^[a-z\d]+(-[a-z\d]+)*$/', $str) === 1) {
120+
return NamingConvention::KEBAB;
121+
}
122+
if (preg_match('/^[A-Z\d]+(-[A-Z\d]+)*$/', $str) === 1) {
123+
return NamingConvention::SCREAMING_KEBAB;
124+
}
125+
if (preg_match('/^[a-z\d]+(\.[a-z\d]+)*$/', $str) === 1) {
126+
return NamingConvention::DOT;
127+
}
128+
if (preg_match('/^[A-Z][a-z\d]*(-[A-Z][a-z\d]*)*$/', $str) === 1) {
129+
return NamingConvention::TRAIN;
130+
}
131+
return null;
64132
}
65133

66-
public static function getNamingStyle(string $str): string
134+
public static function convert(string $input, NamingConvention $target): string
135+
{
136+
$current = self::detect($input);
137+
if ($current === $target) {
138+
return $input;
139+
}
140+
if ($current === null) {
141+
return $input; // не распознан — не трогаем
142+
}
143+
144+
// Промежуточное представление: нормализуем в слова
145+
$words = self::splitToWords($input, $current);
146+
147+
// Собираем в целевой стиль
148+
return self::joinWords($words, $target);
149+
}
150+
151+
private static function splitToWords(string $str, NamingConvention $style): array
152+
{
153+
return match ($style) {
154+
NamingConvention::UPPER_CAMEL, NamingConvention::LOWER_CAMEL =>
155+
self::splitCamelCaseToWords($str),
156+
NamingConvention::SNAKE, NamingConvention::SCREAMING_SNAKE =>
157+
explode('_', $str),
158+
NamingConvention::TRAIN, NamingConvention::KEBAB, NamingConvention::SCREAMING_KEBAB =>
159+
explode('-', $str),
160+
NamingConvention::DOT => explode('.', $str),
161+
};
162+
}
163+
164+
private static function splitCamelCaseToWords(string $str): array
165+
{
166+
$s1 = preg_replace('/(.)([A-Z][a-z]+)/', '$1 $2', $str);
167+
$s2 = preg_replace('/([a-z\d])([A-Z])/', '$1 $2', $s1);
168+
return array_filter(explode(' ', $s2));
169+
}
170+
171+
private static function joinWords(array $words, NamingConvention $style): string
67172
{
68-
return match (true) {
69-
self::isUpperCamelCase($str) => 'UpperCamelCase',
70-
self::isLowerCamelCase($str) => 'lowerCamelCase',
71-
self::isSnakeCase($str) => "snake_case",
72-
self::isScreamingSnakeCase($str) => "SCREAMING_SNAKE_CASE",
73-
default => "Unrecognized",
173+
return match ($style) {
174+
NamingConvention::UPPER_CAMEL =>
175+
implode('', array_map('ucfirst', $words)),
176+
NamingConvention::LOWER_CAMEL =>
177+
lcfirst(implode('', array_map('ucfirst', $words))),
178+
NamingConvention::SNAKE =>
179+
strtolower(implode('_', $words)),
180+
NamingConvention::SCREAMING_SNAKE =>
181+
strtoupper(implode('_', $words)),
182+
NamingConvention::KEBAB =>
183+
strtolower(implode('-', $words)),
184+
NamingConvention::SCREAMING_KEBAB =>
185+
strtoupper(implode('-', $words)),
186+
NamingConvention::TRAIN =>
187+
implode('-', array_map('ucfirst', array_map('strtolower', $words))),
188+
NamingConvention::DOT =>
189+
strtolower(implode('.', $words)),
74190
};
75191
}
76192
}

tests/Unit/NamingStyleTest.php

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
namespace Unit;
33

44
use PHPUnit\Framework\TestCase;
5+
use Sanovskiy\Utility\NamingConvention;
56
use Sanovskiy\Utility\NamingStyle;
67

78
class NamingStyleTest extends TestCase
@@ -54,14 +55,50 @@ public function testNamingStyleEdgeCases()
5455
public function testToSnakeCaseEdgeCases()
5556
{
5657
$this->assertEquals('', NamingStyle::toSnakeCase('')); // Пустая строка
57-
$this->assertEquals('hello_world_123', NamingStyle::toSnakeCase('HelloWorld123')); // Цифры
58-
$this->assertEquals('hello_world_123_abc', NamingStyle::toSnakeCase('HelloWorld123Abc'));
59-
$this->assertEquals('hello_123_world', NamingStyle::toSnakeCase('Hello123World'));
58+
$this->assertEquals('hello_world123', NamingStyle::toSnakeCase('HelloWorld123')); // Цифры
59+
$this->assertEquals('hello_world123_abc', NamingStyle::toSnakeCase('HelloWorld123Abc'));
60+
$this->assertEquals('hello123_world', NamingStyle::toSnakeCase('Hello123World'));
61+
$this->assertEquals('username', NamingStyle::toSnakeCase('username'));
6062
}
6163

6264
public function testToCamelCaseEdgeCases()
6365
{
6466
$this->assertEquals('', NamingStyle::toCamelCase('', true)); // Пустая строка
6567
$this->assertEquals('helloWorld123', NamingStyle::toCamelCase('hello_world_123', false)); // Цифры
68+
$this->assertEquals('username', NamingStyle::toCamelCase('username'));
69+
$this->assertEquals('Username', NamingStyle::toCamelCase('username', true));
70+
}
71+
72+
public function testKebabCase()
73+
{
74+
$this->assertTrue(NamingStyle::isKebabCase('hello-world'));
75+
$this->assertFalse(NamingStyle::isKebabCase('hello_world'));
76+
$this->assertFalse(NamingStyle::isKebabCase('hello--world'));
77+
$this->assertEquals('hello-world', NamingStyle::toKebabCase('hello_world'));
78+
$this->assertEquals('hello-world', NamingStyle::toKebabCase('HelloWorld'));
79+
$this->assertEquals('HELLO-WORLD', NamingStyle::convert('HELLO_WORLD', NamingConvention::SCREAMING_KEBAB));
80+
}
81+
82+
public function testDotCaseAndTrainCase()
83+
{
84+
// Detect
85+
$this->assertTrue(NamingStyle::isDotCase('hello.world'));
86+
$this->assertTrue(NamingStyle::isTrainCase('Hello-World'));
87+
$this->assertFalse(NamingStyle::isDotCase('hello_world'));
88+
$this->assertFalse(NamingStyle::isTrainCase('hello-world')); // kebab, not train
89+
90+
// Convert
91+
$this->assertEquals('hello.world', NamingStyle::toDotCase('hello_world'));
92+
$this->assertEquals('Hello-World', NamingStyle::toTrainCase('hello_world'));
93+
$this->assertEquals('xml.http.request', NamingStyle::toDotCase('XMLHttpRequest'));
94+
$this->assertEquals('Xml-Http-Request', NamingStyle::toTrainCase('XMLHttpRequest'));
95+
96+
// Round-trip
97+
$this->assertEquals('hello.world', NamingStyle::toDotCase('Hello-World'));
98+
$this->assertEquals('Hello-World', NamingStyle::toTrainCase('hello.world'));
99+
100+
// Style name
101+
$this->assertEquals('dot.case', NamingStyle::getNamingStyle('hello.world'));
102+
$this->assertEquals('Train-Case', NamingStyle::getNamingStyle('Hello-World'));
66103
}
67104
}

0 commit comments

Comments
 (0)