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/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..e0f08e36 --- /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 V + * @template L + * @template H + * + * @param V $value + * @param L $min + * @param H $max + * + * @return V|L|H + */ + public static function clamp($value, $min, $max) + { + if (\is_float($min) && \is_nan($min)) { + self::throwValueErrorIfAvailable('clamp(): Argument #2 ($min) cannot be NAN'); + } + + if (\is_float($max) && is_nan($max)) { + self::throwValueErrorIfAvailable('clamp(): Argument #3 ($max) cannot 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..23170966 --- /dev/null +++ b/src/Php86/bootstrap.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($value, $min, $max) { 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..1316f0d7 --- /dev/null +++ b/tests/Php86/Php86Test.php @@ -0,0 +1,97 @@ + + * + * 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 +{ + public function testClamp(): void + { + $this->assertSame(2, clamp(2, 1, 3)); + $this->assertSame(1, clamp(0, 1, 3)); + $this->assertSame(3, clamp(6, 1, 3)); + $this->assertSame(2, clamp(2, 1.3, 3.4)); + $this->assertSame(2.5, clamp(2.5, 1, 3)); + $this->assertSame(2.5, clamp(2.5, 1.3, 3.4)); + $this->assertSame(1.3, clamp(0, 1.3, 3.4)); + $this->assertSame(M_PI, clamp(M_PI, -INF, INF)); + $this->assertTrue(is_nan(clamp(NAN, 4, 6))); + $this->assertSame('c', clamp('a', 'c', 'g')); + $this->assertSame('d', clamp('d', 'c', 'g')); + $this->assertSame('2025-08-15', clamp('2025-08-01', '2025-08-15', '2025-09-15')); + $this->assertSame('2025-08-20', clamp('2025-08-20', '2025-08-15', '2025-09-15')); + $this->assertSame('2025-08-15', clamp(new \DateTimeImmutable('2025-08-01'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15'))->format('Y-m-d')); + $this->assertSame('2025-08-20', clamp(new \DateTimeImmutable('2025-08-20'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15'))->format('Y-m-d')); + $this->assertSame(-1, clamp(null, -1, 1)); + $this->assertSame(1, clamp(null, 1, 3)); + $this->assertSame(-3, clamp(null, -3, -1)); + $this->assertSame(-9999, clamp(-9999, null, 10)); + $this->assertSame(10, clamp(12, null, 10)); + $a = new \InvalidArgumentException('a'); + $b = new \RuntimeException('b'); + $c = new \LogicException('c'); + $this->assertSame($a, clamp($a, $b, $c)); + $this->assertSame($b, clamp($b, $a, $c)); + $this->assertSame($c, clamp($c, $a, $b)); + + $errorMessage = null; + + try { + clamp(4, NAN, 6); + } catch (\ValueError $error) { + $errorMessage = $error->getMessage(); + } + + $this->assertSame('clamp(): Argument #2 ($min) cannot be NAN', $errorMessage); + + $errorMessage = null; + + try { + clamp(7, 6, NAN); + } catch (\ValueError $error) { + $errorMessage = $error->getMessage(); + } + + $this->assertSame('clamp(): Argument #3 ($max) cannot be NAN', $errorMessage); + + $errorMessage = null; + + try { + clamp(1, 3, 2); + } catch (\ValueError $error) { + $errorMessage = $error->getMessage(); + } + + $this->assertSame('clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)', $errorMessage); + + $errorMessage = null; + + try { + clamp(-9999, 5, null); + } catch (\ValueError $error) { + $errorMessage = $error->getMessage(); + } + + $this->assertSame('clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)', $errorMessage); + + $errorMessage = null; + + try { + clamp(12, -5, null); + } catch (\ValueError $error) { + $errorMessage = $error->getMessage(); + } + + $this->assertSame('clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)', $errorMessage); + } +}