├── bootstrap82.php ├── Resources └── stubs │ ├── Deprecated.php │ └── ReflectionConstant.php ├── README.md ├── composer.json ├── LICENSE ├── bootstrap.php └── Php84.php /bootstrap82.php: -------------------------------------------------------------------------------- 1 | 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 | use Symfony\Polyfill\Php84 as p; 13 | 14 | if (\PHP_VERSION_ID >= 80400) { 15 | return; 16 | } 17 | 18 | if (extension_loaded('intl') && !function_exists('grapheme_str_split')) { 19 | function grapheme_str_split(string $string, int $length = 1): array|false { return p\Php84::grapheme_str_split($string, $length); } 20 | } 21 | -------------------------------------------------------------------------------- /Resources/stubs/Deprecated.php: -------------------------------------------------------------------------------- 1 | 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 | if (\PHP_VERSION_ID < 80400) { 13 | #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION | Attribute::TARGET_CLASS_CONSTANT)] 14 | final class Deprecated 15 | { 16 | public readonly ?string $message; 17 | public readonly ?string $since; 18 | 19 | public function __construct(?string $message = null, ?string $since = null) 20 | { 21 | $this->message = $message; 22 | $this->since = $since; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Symfony Polyfill / Php84 2 | ======================== 3 | 4 | This component provides features added to PHP 8.4 core: 5 | 6 | - [`array_find`, `array_find_key`, `array_any` and `array_all`](https://wiki.php.net/rfc/array_find) 7 | - [`bcdivmod`](https://wiki.php.net/rfc/add_bcdivmod_to_bcmath) 8 | - [`Deprecated`](https://wiki.php.net/rfc/deprecated_attribute) 9 | - `CURL_HTTP_VERSION_3` and `CURL_HTTP_VERSION_3ONLY` constants 10 | - [`fpow`](https://wiki.php.net/rfc/raising_zero_to_power_of_negative_number) 11 | - [`grapheme_str_split`](https://wiki.php.net/rfc/grapheme_str_split) 12 | - [`mb_trim`, `mb_ltrim` and `mb_rtrim`](https://wiki.php.net/rfc/mb_trim) 13 | - [`mb_ucfirst` and `mb_lcfirst`](https://wiki.php.net/rfc/mb_ucfirst) 14 | - [`ReflectionConstant`](https://github.com/php/php-src/pull/13669) 15 | 16 | More information can be found in the 17 | [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). 18 | 19 | License 20 | ======= 21 | 22 | This library is released under the [MIT license](LICENSE). 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/polyfill-php84", 3 | "type": "library", 4 | "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", 5 | "keywords": ["polyfill", "shim", "compatibility", "portable"], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Nicolas Grekas", 11 | "email": "p@tchwork.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=7.2" 20 | }, 21 | "autoload": { 22 | "psr-4": { "Symfony\\Polyfill\\Php84\\": "" }, 23 | "files": [ "bootstrap.php" ], 24 | "classmap": [ "Resources/stubs" ] 25 | }, 26 | "minimum-stability": "dev", 27 | "extra": { 28 | "thanks": { 29 | "name": "symfony/polyfill", 30 | "url": "https://github.com/symfony/polyfill" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | 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 | use Symfony\Polyfill\Php84 as p; 13 | 14 | if (\PHP_VERSION_ID >= 80400) { 15 | return; 16 | } 17 | 18 | if (defined('CURL_VERSION_HTTP3') || PHP_VERSION_ID < 80200 && function_exists('curl_version') && curl_version()['version'] >= 0x074200) { // libcurl >= 7.66.0 19 | if (!defined('CURL_HTTP_VERSION_3')) { 20 | define('CURL_HTTP_VERSION_3', 30); 21 | } 22 | 23 | if (!defined('CURL_HTTP_VERSION_3ONLY') && defined('CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256')) { // libcurl >= 7.80.0 (7.88 would be better but is slow to check) 24 | define('CURL_HTTP_VERSION_3ONLY', 31); 25 | } 26 | } 27 | 28 | if (!function_exists('array_find')) { 29 | function array_find(array $array, callable $callback) { return p\Php84::array_find($array, $callback); } 30 | } 31 | 32 | if (!function_exists('array_find_key')) { 33 | function array_find_key(array $array, callable $callback) { return p\Php84::array_find_key($array, $callback); } 34 | } 35 | 36 | if (!function_exists('array_any')) { 37 | function array_any(array $array, callable $callback): bool { return p\Php84::array_any($array, $callback); } 38 | } 39 | 40 | if (!function_exists('array_all')) { 41 | function array_all(array $array, callable $callback): bool { return p\Php84::array_all($array, $callback); } 42 | } 43 | 44 | if (!function_exists('fpow')) { 45 | function fpow(float $num, float $exponent): float { return p\Php84::fpow($num, $exponent); } 46 | } 47 | 48 | if (extension_loaded('mbstring')) { 49 | if (!function_exists('mb_ucfirst')) { 50 | function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Php84::mb_ucfirst($string, $encoding); } 51 | } 52 | 53 | if (!function_exists('mb_lcfirst')) { 54 | function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Php84::mb_lcfirst($string, $encoding); } 55 | } 56 | 57 | if (!function_exists('mb_trim')) { 58 | function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Php84::mb_trim($string, $characters, $encoding); } 59 | } 60 | 61 | if (!function_exists('mb_ltrim')) { 62 | function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Php84::mb_ltrim($string, $characters, $encoding); } 63 | } 64 | 65 | if (!function_exists('mb_rtrim')) { 66 | function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Php84::mb_rtrim($string, $characters, $encoding); } 67 | } 68 | } 69 | 70 | if (extension_loaded('bcmath')) { 71 | if (!function_exists('bcdivmod')) { 72 | function bcdivmod(string $num1, string $num2, ?int $scale = null): ?array { return p\Php84::bcdivmod($num1, $num2, $scale); } 73 | } 74 | } 75 | 76 | if (\PHP_VERSION_ID >= 80200) { 77 | return require __DIR__.'/bootstrap82.php'; 78 | } 79 | 80 | if (extension_loaded('intl') && !function_exists('grapheme_str_split')) { 81 | function grapheme_str_split(string $string, int $length = 1) { return p\Php84::grapheme_str_split($string, $length); } 82 | } 83 | -------------------------------------------------------------------------------- /Resources/stubs/ReflectionConstant.php: -------------------------------------------------------------------------------- 1 | 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 | if (\PHP_VERSION_ID < 80400) { 13 | // @author Daniel Scherzer 14 | final class ReflectionConstant 15 | { 16 | /** 17 | * @var string 18 | * 19 | * @readonly 20 | */ 21 | public $name; 22 | 23 | private $value; 24 | private $deprecated; 25 | 26 | private static $persistentConstants = []; 27 | 28 | public function __construct(string $name) 29 | { 30 | if (!defined($name) || false !== strpos($name, '::')) { 31 | throw new ReflectionException("Constant \"$name\" does not exist"); 32 | } 33 | 34 | $this->name = ltrim($name, '\\'); 35 | $deprecated = false; 36 | $eh = set_error_handler(static function ($type, $msg, $file, $line) use ($name, &$deprecated, &$eh) { 37 | if (\E_DEPRECATED === $type && "Constant $name is deprecated" === $msg) { 38 | return $deprecated = true; 39 | } 40 | 41 | return $eh && $eh($type, $msg, $file, $line); 42 | }); 43 | 44 | try { 45 | $this->value = constant($name); 46 | $this->deprecated = $deprecated; 47 | } finally { 48 | restore_error_handler(); 49 | } 50 | } 51 | 52 | public function getName(): string 53 | { 54 | return $this->name; 55 | } 56 | 57 | public function getValue() 58 | { 59 | return $this->value; 60 | } 61 | 62 | public function getNamespaceName(): string 63 | { 64 | if (false === $slashPos = strrpos($this->name, '\\')) { 65 | return ''; 66 | } 67 | 68 | return substr($this->name, 0, $slashPos); 69 | } 70 | 71 | public function getShortName(): string 72 | { 73 | if (false === $slashPos = strrpos($this->name, '\\')) { 74 | return $this->name; 75 | } 76 | 77 | return substr($this->name, $slashPos + 1); 78 | } 79 | 80 | public function isDeprecated(): bool 81 | { 82 | return $this->deprecated; 83 | } 84 | 85 | public function __toString(): string 86 | { 87 | // A constant is persistent if provided by PHP itself rather than 88 | // being defined by users. If we got here, we know that it *is* 89 | // defined, so we just need to figure out if it is defined by the 90 | // user or not 91 | if (!self::$persistentConstants) { 92 | $persistentConstants = get_defined_constants(true); 93 | unset($persistentConstants['user']); 94 | foreach ($persistentConstants as $constants) { 95 | self::$persistentConstants += $constants; 96 | } 97 | } 98 | $persistent = array_key_exists($this->name, self::$persistentConstants); 99 | 100 | // Can't match the inclusion of `no_file_cache` but the rest is 101 | // possible to match 102 | $result = 'Constant [ '; 103 | if ($persistent || $this->deprecated) { 104 | $result .= '<'; 105 | if ($persistent) { 106 | $result .= 'persistent'; 107 | if ($this->deprecated) { 108 | $result .= ', '; 109 | } 110 | } 111 | if ($this->deprecated) { 112 | $result .= 'deprecated'; 113 | } 114 | $result .= '> '; 115 | } 116 | // Cannot just use gettype() to match zend_zval_type_name() 117 | if (is_object($this->value)) { 118 | $result .= \PHP_VERSION_ID >= 80000 ? get_debug_type($this->value) : gettype($this->value); 119 | } elseif (is_bool($this->value)) { 120 | $result .= 'bool'; 121 | } elseif (is_int($this->value)) { 122 | $result .= 'int'; 123 | } elseif (is_float($this->value)) { 124 | $result .= 'float'; 125 | } elseif (null === $this->value) { 126 | $result .= 'null'; 127 | } else { 128 | $result .= gettype($this->value); 129 | } 130 | $result .= ' '; 131 | $result .= $this->name; 132 | $result .= ' ] { '; 133 | if (is_array($this->value)) { 134 | $result .= 'Array'; 135 | } else { 136 | // This will throw an exception if the value is an object that 137 | // cannot be converted to string; that is expected and matches 138 | // the behavior of zval_get_string_func() 139 | $result .= (string) $this->value; 140 | } 141 | $result .= " }\n"; 142 | 143 | return $result; 144 | } 145 | 146 | public function __sleep(): array 147 | { 148 | throw new Exception("Serialization of 'ReflectionConstant' is not allowed"); 149 | } 150 | 151 | public function __wakeup(): void 152 | { 153 | throw new Exception("Unserialization of 'ReflectionConstant' is not allowed"); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Php84.php: -------------------------------------------------------------------------------- 1 | 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\Polyfill\Php84; 13 | 14 | /** 15 | * @author Ayesh Karunaratne 16 | * @author Pierre Ambroise 17 | * 18 | * @internal 19 | */ 20 | final class Php84 21 | { 22 | public static function mb_ucfirst(string $string, ?string $encoding = null): string 23 | { 24 | if (null === $encoding) { 25 | $encoding = mb_internal_encoding(); 26 | } 27 | 28 | try { 29 | $validEncoding = @mb_check_encoding('', $encoding); 30 | } catch (\ValueError $e) { 31 | throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); 32 | } 33 | 34 | // BC for PHP 7.3 and lower 35 | if (!$validEncoding) { 36 | throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); 37 | } 38 | 39 | $firstChar = mb_substr($string, 0, 1, $encoding); 40 | $firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding); 41 | 42 | return $firstChar.mb_substr($string, 1, null, $encoding); 43 | } 44 | 45 | public static function mb_lcfirst(string $string, ?string $encoding = null): string 46 | { 47 | if (null === $encoding) { 48 | $encoding = mb_internal_encoding(); 49 | } 50 | 51 | try { 52 | $validEncoding = @mb_check_encoding('', $encoding); 53 | } catch (\ValueError $e) { 54 | throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); 55 | } 56 | 57 | // BC for PHP 7.3 and lower 58 | if (!$validEncoding) { 59 | throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); 60 | } 61 | 62 | $firstChar = mb_substr($string, 0, 1, $encoding); 63 | $firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding); 64 | 65 | return $firstChar.mb_substr($string, 1, null, $encoding); 66 | } 67 | 68 | public static function array_find(array $array, callable $callback) 69 | { 70 | foreach ($array as $key => $value) { 71 | if ($callback($value, $key)) { 72 | return $value; 73 | } 74 | } 75 | 76 | return null; 77 | } 78 | 79 | public static function array_find_key(array $array, callable $callback) 80 | { 81 | foreach ($array as $key => $value) { 82 | if ($callback($value, $key)) { 83 | return $key; 84 | } 85 | } 86 | 87 | return null; 88 | } 89 | 90 | public static function array_any(array $array, callable $callback): bool 91 | { 92 | foreach ($array as $key => $value) { 93 | if ($callback($value, $key)) { 94 | return true; 95 | } 96 | } 97 | 98 | return false; 99 | } 100 | 101 | public static function array_all(array $array, callable $callback): bool 102 | { 103 | foreach ($array as $key => $value) { 104 | if (!$callback($value, $key)) { 105 | return false; 106 | } 107 | } 108 | 109 | return true; 110 | } 111 | 112 | public static function fpow(float $num, float $exponent): float 113 | { 114 | return $num ** $exponent; 115 | } 116 | 117 | public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string 118 | { 119 | return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__); 120 | } 121 | 122 | public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string 123 | { 124 | return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__); 125 | } 126 | 127 | public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string 128 | { 129 | return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__); 130 | } 131 | 132 | private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function): string 133 | { 134 | if (null === $encoding) { 135 | $encoding = mb_internal_encoding(); 136 | } 137 | 138 | try { 139 | $validEncoding = @mb_check_encoding('', $encoding); 140 | } catch (\ValueError $e) { 141 | throw new \ValueError(sprintf('%s(): Argument #3 ($encoding) must be a valid encoding, "%s" given', $function, $encoding)); 142 | } 143 | 144 | // BC for PHP 7.3 and lower 145 | if (!$validEncoding) { 146 | throw new \ValueError(sprintf('%s(): Argument #3 ($encoding) must be a valid encoding, "%s" given', $function, $encoding)); 147 | } 148 | 149 | if ('' === $characters) { 150 | return null === $encoding ? $string : mb_convert_encoding($string, $encoding); 151 | } 152 | 153 | if ('UTF-8' === $encoding || \in_array(strtolower($encoding), ['utf-8', 'utf8'], true)) { 154 | $encoding = 'UTF-8'; 155 | } 156 | 157 | $string = mb_convert_encoding($string, 'UTF-8', $encoding); 158 | 159 | if (null !== $characters) { 160 | $characters = mb_convert_encoding($characters, 'UTF-8', $encoding); 161 | } 162 | 163 | if (null === $characters) { 164 | $characters = "\\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}"; 165 | } else { 166 | $characters = preg_quote($characters); 167 | } 168 | 169 | $string = preg_replace(sprintf($regex, $characters), '', $string); 170 | 171 | if ('UTF-8' === $encoding) { 172 | return $string; 173 | } 174 | 175 | return mb_convert_encoding($string, $encoding, 'UTF-8'); 176 | } 177 | 178 | public static function grapheme_str_split(string $string, int $length) 179 | { 180 | if (0 > $length || 1073741823 < $length) { 181 | throw new \ValueError('grapheme_str_split(): Argument #2 ($length) must be greater than 0 and less than or equal to 1073741823.'); 182 | } 183 | 184 | if ('' === $string) { 185 | return []; 186 | } 187 | 188 | $regex = ((float) \PCRE_VERSION < 10 ? (float) \PCRE_VERSION >= 8.32 : (float) \PCRE_VERSION >= 10.39) 189 | ? '\X' 190 | : '(?:\r\n|(?:[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-ᅟ]*(?:[가개갸걔거게겨계고과괘괴교구궈궤귀규그긔기까깨꺄꺠꺼께껴꼐꼬꽈꽤꾀꾜꾸꿔꿰뀌뀨끄끠끼나내냐냬너네녀녜노놔놰뇌뇨누눠눼뉘뉴느늬니다대댜댸더데뎌뎨도돠돼되됴두둬뒈뒤듀드듸디따때땨떄떠떼뗘뗴또똬뙈뙤뚀뚜뚸뛔뛰뜌뜨띄띠라래랴럐러레려례로롸뢔뢰료루뤄뤠뤼류르릐리마매먀먜머메며몌모뫄뫠뫼묘무뭐뭬뮈뮤므믜미바배뱌뱨버베벼볘보봐봬뵈뵤부붜붸뷔뷰브븨비빠빼뺘뺴뻐뻬뼈뼤뽀뽜뽸뾔뾰뿌뿨쀄쀠쀼쁘쁴삐사새샤섀서세셔셰소솨쇄쇠쇼수숴쉐쉬슈스싀시싸쌔쌰썌써쎄쎠쎼쏘쏴쐐쐬쑈쑤쒀쒜쒸쓔쓰씌씨아애야얘어에여예오와왜외요우워웨위유으의이자재쟈쟤저제져졔조좌좨죄죠주줘줴쥐쥬즈즤지짜째쨔쨰쩌쩨쪄쪠쪼쫘쫴쬐쬬쭈쭤쮀쮜쮸쯔쯰찌차채챠챼처체쳐쳬초촤쵀최쵸추춰췌취츄츠츼치카캐캬컈커케켜켸코콰쾌쾨쿄쿠쿼퀘퀴큐크킈키타태탸턔터테텨톄토톼퇘퇴툐투퉈퉤튀튜트틔티파패퍄퍠퍼페펴폐포퐈퐤푀표푸풔풰퓌퓨프픠피하해햐햬허헤혀혜호화홰회효후훠훼휘휴흐희히]?[ᅠ-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-ᅟ]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{200D}\x{1D165}\x{1D16E}-\x{1D172}]*|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}])'; 191 | 192 | if (!preg_match_all('/'. $regex .'/u', $string, $matches)) { 193 | return false; 194 | } 195 | 196 | if (1 === $length) { 197 | return $matches[0]; 198 | } 199 | 200 | $chunks = array_chunk($matches[0], $length); 201 | foreach ($chunks as &$chunk) { 202 | $chunk = implode('', $chunk); 203 | } 204 | 205 | return $chunks; 206 | } 207 | 208 | public static function bcdivmod(string $num1, string $num2, ?int $scale = null): ?array 209 | { 210 | if (null === $quot = \bcdiv($num1, $num2, 0)) { 211 | return null; 212 | } 213 | $scale = $scale ?? (\PHP_VERSION_ID >= 70300 ? \bcscale() : (ini_get('bcmath.scale') ?: 0)); 214 | 215 | return [$quot, \bcmod($num1, $num2, $scale)]; 216 | } 217 | } 218 | --------------------------------------------------------------------------------