Skip to content

Commit dc01ec2

Browse files
committed
[mbstring][PHP 8.4] Add mb_ucfirst and mb_lcfirst to polyfills
Adds polyfills for `mb_ucfirst` and `mb_lcfirst` functions based on the polyfill shown in PHP.Watch. It basically takes the first mb character, calls `mb_convert_case` with `MB_CASE_TITLE` or `MB_CASE_LOWER`, and returns with the concat of the remaining string. The tests are taken from the php-src PR. - [RFC](https://wiki.php.net/rfc/mb_ucfirst) - [php-src PR: php-src#13161](php/php-src#13161) - [PHP.Watch - PHP 8.4: New `mb_ucfirst` and `mb_lcfirst` functions](https://php.watch/versions/8.4/mb_ucfirst-mb_ucfirst)
1 parent 6a71d4e commit dc01ec2

File tree

9 files changed

+220
-0
lines changed

9 files changed

+220
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Polyfills are provided for:
6767
- the `str_increment` and `str_decrement` functions introduced in PHP 8.3;
6868
- the `Date*Exception/Error` classes introduced in PHP 8.3;
6969
- the `SQLite3Exception` class introduced in PHP 8.3;
70+
- the `mb_ucfirst` and `mb_lcfirst` functions introduced in PHP 8.4;
7071

7172
It is strongly recommended to upgrade your PHP version and/or install the missing
7273
extensions whenever possible. This polyfill should be used only when there is no

src/Mbstring/Mbstring.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,49 @@ public static function mb_str_pad(string $string, int $length, string $pad_strin
871871
}
872872
}
873873

874+
public static function mb_ucfirst(string $string, ?string $encoding = null): string {
875+
if (null === $encoding) {
876+
$encoding = self::mb_internal_encoding();
877+
}
878+
879+
try {
880+
$validEncoding = @self::mb_check_encoding('', $encoding);
881+
} catch (\ValueError $e) {
882+
throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
883+
}
884+
885+
// BC for PHP 7.3 and lower
886+
if (!$validEncoding) {
887+
throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
888+
}
889+
890+
$firstChar = mb_substr($string, 0, 1, $encoding);
891+
$firstChar = mb_convert_case($firstChar, MB_CASE_TITLE, $encoding);
892+
893+
return $firstChar . mb_substr($string, 1, null, $encoding);
894+
}
895+
896+
public static function mb_lcfirst(string $string, ?string $encoding = null): string {
897+
if (null === $encoding) {
898+
$encoding = self::mb_internal_encoding();
899+
}
900+
901+
try {
902+
$validEncoding = @self::mb_check_encoding('', $encoding);
903+
} catch (\ValueError $e) {
904+
throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
905+
}
906+
907+
// BC for PHP 7.3 and lower
908+
if (!$validEncoding) {
909+
throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
910+
}
911+
$firstChar = mb_substr($string, 0, 1, $encoding);
912+
$firstChar = mb_convert_case($firstChar, MB_CASE_LOWER, $encoding);
913+
914+
return $firstChar . mb_substr($string, 1, null, $encoding);
915+
}
916+
874917
private static function getSubpart($pos, $part, $haystack, $encoding)
875918
{
876919
if (false === $pos) {

src/Mbstring/bootstrap.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,14 @@ function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstrin
136136
function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); }
137137
}
138138

139+
if (!function_exists('mb_ucfirst')) {
140+
function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); }
141+
}
142+
143+
if (!function_exists('mb_lcfirst')) {
144+
function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); }
145+
}
146+
139147
if (extension_loaded('mbstring')) {
140148
return;
141149
}

src/Mbstring/bootstrap80.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,14 @@ function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = nul
132132
function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); }
133133
}
134134

135+
if (!function_exists('mb_ucfirst')) {
136+
function mb_ucfirst($string, ?string $encoding = null): string { return p\Mbstring::mb_ucfirst($string, $encoding); }
137+
}
138+
139+
if (!function_exists('mb_lcfirst')) {
140+
function mb_lcfirst($string, ?string $encoding = null): string { return p\Mbstring::mb_lcfirst($string, $encoding); }
141+
}
142+
135143
if (extension_loaded('mbstring')) {
136144
return;
137145
}

src/Php84/Php84.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,47 @@
1818
*/
1919
final class Php84
2020
{
21+
public static function mb_ucfirst(string $string, ?string $encoding = null): string {
22+
if (null === $encoding) {
23+
$encoding = mb_internal_encoding();
24+
}
25+
26+
try {
27+
$validEncoding = @mb_check_encoding('', $encoding);
28+
} catch (\ValueError $e) {
29+
throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
30+
}
31+
32+
// BC for PHP 7.3 and lower
33+
if (!$validEncoding) {
34+
throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
35+
}
36+
37+
$firstChar = mb_substr($string, 0, 1, $encoding);
38+
$firstChar = mb_convert_case($firstChar, MB_CASE_TITLE, $encoding);
39+
40+
return $firstChar . mb_substr($string, 1, null, $encoding);
41+
}
42+
43+
public static function mb_lcfirst(string $string, ?string $encoding = null): string {
44+
if (null === $encoding) {
45+
$encoding = mb_internal_encoding();
46+
}
47+
48+
try {
49+
$validEncoding = @mb_check_encoding('', $encoding);
50+
} catch (\ValueError $e) {
51+
throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
52+
}
53+
54+
// BC for PHP 7.3 and lower
55+
if (!$validEncoding) {
56+
throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding));
57+
}
58+
59+
$firstChar = mb_substr($string, 0, 1, $encoding);
60+
$firstChar = mb_convert_case($firstChar, MB_CASE_LOWER, $encoding);
61+
62+
return $firstChar . mb_substr($string, 1, null, $encoding);
63+
}
2164
}

src/Php84/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ Symfony Polyfill / Php84
33

44
This component provides features added to PHP 8.4 core:
55

6+
- [`mb_ucfirst` and `mb_lcfirst`](https://wiki.php.net/rfc/mb_ucfirst)
7+
68
More information can be found in the
79
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
810

src/Php84/bootstrap.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,12 @@
1414
if (\PHP_VERSION_ID >= 80400) {
1515
return;
1616
}
17+
18+
19+
if (!function_exists('mb_ucfirst')) {
20+
function mb_ucfirst($string, ?string $encoding = null): string { return p\Php84::mb_ucfirst($string, $encoding); }
21+
}
22+
23+
if (!function_exists('mb_lcfirst')) {
24+
function mb_lcfirst($string, ?string $encoding = null): string { return p\Php84::mb_lcfirst($string, $encoding); }
25+
}

tests/Mbstring/MbstringTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,20 @@ public function testMbStrPadInvalidArguments(string $expectedError, string $stri
657657
mb_str_pad($string, $length, $padString, $padType, $encoding);
658658
}
659659

660+
/**
661+
* @dataProvider ucFirstDataProvider
662+
*/
663+
public function testMbUcFirst(string $string, string $expected): void {
664+
$this->assertSame($expected, mb_ucfirst($string));
665+
}
666+
667+
/**
668+
* @dataProvider lcFirstDataProvider
669+
*/
670+
public function testMbLcFirst(string $string, string $expected): void {
671+
$this->assertSame($expected, mb_lcfirst($string));
672+
}
673+
660674
public static function paddingStringProvider(): iterable
661675
{
662676
// Simple ASCII strings
@@ -727,4 +741,43 @@ public static function mbStrPadInvalidArgumentsProvider(): iterable
727741
yield ['mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH', '▶▶', 6, ' ', 123456];
728742
yield ['mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "unexisting" given', '▶▶', 6, ' ', \STR_PAD_BOTH, 'unexisting'];
729743
}
744+
745+
public static function ucFirstDataProvider(): array {
746+
return [
747+
['', ''],
748+
['test', 'Test'],
749+
['TEST', 'TEST'],
750+
['TesT', 'TesT'],
751+
['ab', 'Ab'],
752+
['ABS', 'ABS'],
753+
['đắt quá!', 'Đắt quá!'],
754+
['აბგ', 'აბგ'],
755+
['lj', 'Lj'],
756+
["\u{01CA}", "\u{01CB}"],
757+
["\u{01CA}\u{01CA}", "\u{01CB}\u{01CA}"],
758+
["łámał", "Łámał"],
759+
// Full case-mapping and case-folding that changes the length of the string only supported
760+
// in PHP > 7.3.
761+
["ßst", PHP_VERSION_ID < 70300 ? "ßst" : "Ssst"],
762+
];
763+
}
764+
765+
public static function lcFirstDataProvider(): array {
766+
return [
767+
['', ''],
768+
['test', 'test'],
769+
['Test', 'test'],
770+
['tEST', 'tEST'],
771+
['Ab', 'ab'],
772+
['ABS', 'aBS'],
773+
['Đắt quá!', 'đắt quá!'],
774+
['აბგ', 'აბგ'],
775+
['Lj', PHP_VERSION_ID < 70200 ? 'Lj' : 'lj'],
776+
["\u{01CB}", PHP_VERSION_ID < 70200 ? "\u{01CB}" : "\u{01CC}"],
777+
["\u{01CA}", "\u{01CC}"],
778+
["\u{01CA}\u{01CA}", "\u{01CC}\u{01CA}"],
779+
["\u{212A}\u{01CA}", "\u{006b}\u{01CA}"],
780+
["ß", "ß"],
781+
];
782+
}
730783
}

tests/Php84/Php84Test.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,61 @@
1111

1212
namespace Symfony\Polyfill\Tests\Php84;
1313

14+
use PHPUnit\Framework\Attributes\DataProvider;
1415
use PHPUnit\Framework\TestCase;
1516

1617
class Php84Test extends TestCase
1718
{
19+
/**
20+
* @dataProvider ucFirstDataProvider
21+
*/
22+
public function testMbUcFirst(string $string, string $expected): void {
23+
$this->assertSame($expected, mb_ucfirst($string));
24+
}
25+
26+
/**
27+
* @dataProvider lcFirstDataProvider
28+
*/
29+
public function testMbLcFirst(string $string, string $expected): void {
30+
$this->assertSame($expected, mb_lcfirst($string));
31+
}
32+
33+
public static function ucFirstDataProvider(): array {
34+
return [
35+
['', ''],
36+
['test', 'Test'],
37+
['TEST', 'TEST'],
38+
['TesT', 'TesT'],
39+
['ab', 'Ab'],
40+
['ABS', 'ABS'],
41+
['đắt quá!', 'Đắt quá!'],
42+
['აბგ', 'აბგ'],
43+
['lj', 'Lj'],
44+
["\u{01CA}", "\u{01CB}"],
45+
["\u{01CA}\u{01CA}", "\u{01CB}\u{01CA}"],
46+
["łámał", "Łámał"],
47+
// Full case-mapping and case-folding that changes the length of the string only supported
48+
// in PHP > 7.3.
49+
["ßst", PHP_VERSION_ID < 70300 ? "ßst" : "Ssst"],
50+
];
51+
}
52+
53+
public static function lcFirstDataProvider(): array {
54+
return [
55+
['', ''],
56+
['test', 'test'],
57+
['Test', 'test'],
58+
['tEST', 'tEST'],
59+
['Ab', 'ab'],
60+
['ABS', 'aBS'],
61+
['Đắt quá!', 'đắt quá!'],
62+
['აბგ', 'აბგ'],
63+
['Lj', PHP_VERSION_ID < 70200 ? 'Lj' : 'lj'],
64+
["\u{01CB}", PHP_VERSION_ID < 70200 ? "\u{01CB}" : "\u{01CC}"],
65+
["\u{01CA}", "\u{01CC}"],
66+
["\u{01CA}\u{01CA}", "\u{01CC}\u{01CA}"],
67+
["\u{212A}\u{01CA}", "\u{006b}\u{01CA}"],
68+
["ß", "ß"],
69+
];
70+
}
1871
}

0 commit comments

Comments
 (0)