From 2b4c47dc4c146844495d738b7c19af8395fda91c Mon Sep 17 00:00:00 2001 From: kylekatarnls Date: Wed, 19 Nov 2025 11:40:37 +0100 Subject: [PATCH] [8.6] Add `clamp` function --- README.md | 1 + composer.json | 1 + splitsh.json | 1 + src/Php86/LICENSE | 19 ++++ src/Php86/Php86.php | 65 ++++++++++++++ src/Php86/README.md | 14 +++ src/Php86/bootstrap.php | 35 ++++++++ src/Php86/bootstrap80.php | 31 +++++++ src/Php86/composer.json | 32 +++++++ src/bootstrap.php | 4 + tests/Php86/Php86Test.php | 176 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 379 insertions(+) create mode 100644 src/Php86/LICENSE create mode 100644 src/Php86/Php86.php create mode 100644 src/Php86/README.md create mode 100644 src/Php86/bootstrap.php create mode 100644 src/Php86/bootstrap80.php create mode 100644 src/Php86/composer.json create mode 100644 tests/Php86/Php86Test.php diff --git a/README.md b/README.md index 60edd9a4..27f52bf3 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Polyfills are provided for: - the `NoDiscard` attribute introduced in PHP 8.5; - the `array_first` and `array_last` functions introduced in PHP 8.5; - the `DelayedTargetValidation` attribute introduced in PHP 8.5; +- the `clamp` function introduced in PHP 8.6; It is strongly recommended to upgrade your PHP version and/or install the missing extensions whenever possible. This polyfill should be used only when there is no diff --git a/composer.json b/composer.json index bfd95859..2997fb66 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "symfony/polyfill-php83": "self.version", "symfony/polyfill-php84": "self.version", "symfony/polyfill-php85": "self.version", + "symfony/polyfill-php86": "self.version", "symfony/polyfill-iconv": "self.version", "symfony/polyfill-intl-grapheme": "self.version", "symfony/polyfill-intl-icu": "self.version", diff --git a/splitsh.json b/splitsh.json index 98887bfd..b2b03e46 100644 --- a/splitsh.json +++ b/splitsh.json @@ -10,6 +10,7 @@ "polyfill-php83": "src/Php83", "polyfill-php84": "src/Php84", "polyfill-php85": "src/Php85", + "polyfill-php86": "src/Php86", "polyfill-iconv": "src/Iconv", "polyfill-intl-grapheme": "src/Intl/Grapheme", "polyfill-intl-icu": "src/Intl/Icu", diff --git a/src/Php86/LICENSE b/src/Php86/LICENSE new file mode 100644 index 00000000..bc38d714 --- /dev/null +++ b/src/Php86/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2025-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Php86/Php86.php b/src/Php86/Php86.php new file mode 100644 index 00000000..fe55ae2f --- /dev/null +++ b/src/Php86/Php86.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php86; + +/** + * @author kylekatarnls + * + * @internal + */ +final class Php86 +{ + /** + * @template Value + * @template Minimum + * @template Maximum + * + * @param Value $value + * @param Minimum $min + * @param Maximum $max + * + * @return Value|Minimum|Maximum + */ + public static function clamp($value, $min, $max) + { + if (\is_float($min) && is_nan($min)) { + self::throwValueErrorIfAvailable('clamp(): Argument #2 ($min) must not be NAN'); + } + + if (\is_float($max) && is_nan($max)) { + self::throwValueErrorIfAvailable('clamp(): Argument #3 ($max) must not be NAN'); + } + + if ($max < $min) { + self::throwValueErrorIfAvailable('clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)'); + } + + if ($value > $max) { + return $max; + } + + if ($value < $min) { + return $min; + } + + return $value; + } + + private static function throwValueErrorIfAvailable(string $message): void + { + if (!class_exists(\ValueError::class)) { + throw new \InvalidArgumentException($message); + } + + throw new \ValueError($message); + } +} diff --git a/src/Php86/README.md b/src/Php86/README.md new file mode 100644 index 00000000..2087e342 --- /dev/null +++ b/src/Php86/README.md @@ -0,0 +1,14 @@ +Symfony Polyfill / Php86 +======================== + +This component provides features added to PHP 8.6 core: + +- [`clamp`](https://wiki.php.net/rfc/clamp_v2) + +More information can be found in the +[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). + +License +======= + +This library is released under the [MIT license](LICENSE). diff --git a/src/Php86/bootstrap.php b/src/Php86/bootstrap.php new file mode 100644 index 00000000..69864609 --- /dev/null +++ b/src/Php86/bootstrap.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php86 as p; + +if (\PHP_VERSION_ID >= 80600) { + return; +} + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!function_exists('clamp')) { + /** + * @template V + * @template L + * @template H + * + * @param V $value + * @param L $min + * @param H $max + * + * @return V|L|H + */ + function clamp($value, $min, $max) { return p\Php86::clamp($value, $min, $max); } +} diff --git a/src/Php86/bootstrap80.php b/src/Php86/bootstrap80.php new file mode 100644 index 00000000..c90afc7d --- /dev/null +++ b/src/Php86/bootstrap80.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php86 as p; + +if (\PHP_VERSION_ID >= 80600) { + return; +} + +if (!function_exists('clamp')) { + /** + * @template V + * @template L + * @template H + * + * @param V $value + * @param L $min + * @param H $max + * + * @return V|L|H + */ + function clamp(mixed $value, mixed $min, mixed $max): mixed { return p\Php86::clamp($value, $min, $max); } +} diff --git a/src/Php86/composer.json b/src/Php86/composer.json new file mode 100644 index 00000000..e13cd9d8 --- /dev/null +++ b/src/Php86/composer.json @@ -0,0 +1,32 @@ +{ + "name": "symfony/polyfill-php86", + "type": "library", + "description": "Symfony polyfill backporting some PHP 8.6+ features to lower PHP versions", + "keywords": ["polyfill", "shim", "compatibility", "portable"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=7.2" + }, + "autoload": { + "psr-4": { "Symfony\\Polyfill\\Php86\\": "" }, + "files": [ "bootstrap.php" ] + }, + "minimum-stability": "dev", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + } +} diff --git a/src/bootstrap.php b/src/bootstrap.php index aa7e46e3..e65fd944 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -40,3 +40,7 @@ if (\PHP_VERSION_ID < 80500) { require __DIR__.'/Php85/bootstrap.php'; } + +if (\PHP_VERSION_ID < 80600) { + require __DIR__.'/Php86/bootstrap.php'; +} diff --git a/tests/Php86/Php86Test.php b/tests/Php86/Php86Test.php new file mode 100644 index 00000000..d2a20313 --- /dev/null +++ b/tests/Php86/Php86Test.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Tests\Php86; + +use PHPUnit\Framework\TestCase; + +class Php86Test extends TestCase +{ + /** + * @dataProvider provideValidClampInput + */ + public function testClampSuccess(array $arguments, $result): void + { + [$value, $min, $max] = $arguments; + + $actual = clamp($value, $min, $max); + + if ($value instanceof \DateTimeImmutable) { + $this->assertInstanceOf(\DateTimeImmutable::class, $actual); + + $actual = $actual->format('Y-m-d'); + } + + $this->assertSame($result, $actual); + } + + public function testClampNanReturn(): void + { + $this->assertTrue(is_nan(clamp(NAN, 4, 6))); + } + + public static function provideValidClampInput(): array + { + $a = new \InvalidArgumentException('a'); + $b = new \RuntimeException('b'); + $c = new \LogicException('c'); + + return [ + [ + [2, 1, 3], + 2, + ], + [ + [0, 1, 3], + 1, + ], + [ + [6, 1, 3], + 3, + ], + [ + [2, 1.3, 3.4], + 2, + ], + [ + [2.5, 1, 3], + 2.5, + ], + [ + [2.5, 1.3, 3.4], + 2.5, + ], + [ + [0, 1.3, 3.4], + 1.3, + ], + [ + [M_PI, -INF, INF], + M_PI, + ], + [ + ['a', 'c', 'g'], + 'c', + ], + [ + ['d', 'c', 'g'], + 'd', + ], + [ + ['2025-08-01', '2025-08-15', '2025-09-15'], + '2025-08-15', + ], + [ + ['2025-08-20', '2025-08-15', '2025-09-15'], + '2025-08-20', + ], + [ + [new \DateTimeImmutable('2025-08-01'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15')], + '2025-08-15', + ], + [ + [new \DateTimeImmutable('2025-08-20'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15')], + '2025-08-20', + ], + [ + [null, -1, 1], + -1, + ], + [ + [null, 1, 3], + 1, + ], + [ + [null, -3, -1], + -3, + ], + [ + [-9999, null, 10], + -9999, + ], + [ + [12, null, 10], + 10, + ], + [ + [$a, $b, $c], + $a, + ], + [ + [$b, $a, $c], + $b, + ], + [ + [$c, $a, $b], + $c, + ], + ]; + } + + /** + * @dataProvider provideInvalidClampInput + */ + public function testClampFailure(array $arguments, string $error): void + { + $this->expectException(\ValueError::class); + $this->expectExceptionMessage($error); + + [$value, $min, $max] = $arguments; + clamp($value, $min, $max); + } + + public static function provideInvalidClampInput(): array + { + return [ + [ + [4, NAN, 6], + 'clamp(): Argument #2 ($min) must not be NAN', + ], + [ + [7, 6, NAN], + 'clamp(): Argument #3 ($max) must not be NAN', + ], + [ + [1, 3, 2], + 'clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)', + ], + [ + [-9999, 5, null], + 'clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)', + ], + [ + [12, -5, null], + 'clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)', + ], + ]; + } +}