diff --git a/README.md b/README.md index 9b2885a50..5f6391fca 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Polyfills are provided for: - the `array_find`, `array_find_key`, `array_any` and `array_all` functions introduced in PHP 8.4; - the `Deprecated` attribute introduced in PHP 8.4; - the `mb_trim`, `mb_ltrim` and `mb_rtrim` functions introduced in PHP 8.4; +- the `ReflectionConstant` class introduced in PHP 8.4 - the `CURL_HTTP_VERSION_3` and `CURL_HTTP_VERSION_3ONLY` constants introduced in PHP 8.4; - the `grapheme_str_split` function introduced in PHP 8.4; - the `bcdivmod` function introduced in PHP 8.4; diff --git a/src/Php84/Php84.php b/src/Php84/Php84.php index defa2fd10..1eea63afb 100644 --- a/src/Php84/Php84.php +++ b/src/Php84/Php84.php @@ -210,7 +210,7 @@ public static function bcdivmod(string $num1, string $num2, ?int $scale = null): if (null === $quot = \bcdiv($num1, $num2, 0)) { return null; } - $scale = $scale ?? (\PHP_VERSION_ID >= 70300 ? \bcscale() : (ini_get('bcmath.scale') ?: 0); + $scale = $scale ?? (\PHP_VERSION_ID >= 70300 ? \bcscale() : (ini_get('bcmath.scale') ?: 0)); return [$quot, \bcmod($num1, $num2, $scale)]; } diff --git a/src/Php84/README.md b/src/Php84/README.md index 39493600e..15445c6de 100644 --- a/src/Php84/README.md +++ b/src/Php84/README.md @@ -11,6 +11,7 @@ This component provides features added to PHP 8.4 core: - [`grapheme_str_split`](https://wiki.php.net/rfc/grapheme_str_split) - [`mb_trim`, `mb_ltrim` and `mb_rtrim`](https://wiki.php.net/rfc/mb_trim) - [`mb_ucfirst` and `mb_lcfirst`](https://wiki.php.net/rfc/mb_ucfirst) +- [`ReflectionConstant`](https://github.com/php/php-src/pull/13669) More information can be found in the [main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md). diff --git a/src/Php84/Resources/stubs/ReflectionConstant.php b/src/Php84/Resources/stubs/ReflectionConstant.php new file mode 100644 index 000000000..f4c8448bf --- /dev/null +++ b/src/Php84/Resources/stubs/ReflectionConstant.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80400) { + /** + * @author Daniel Scherzer + */ + final class ReflectionConstant + { + /** + * @var string + * + * @readonly + */ + public $name; + + private $value; + private $deprecated; + + private static $persistentConstants = []; + + public function __construct(string $name) + { + if (!defined($name) || false !== strpos($name, '::')) { + throw new ReflectionException("Constant \"$name\" does not exist"); + } + + $this->name = ltrim($name, '\\'); + $deprecated = false; + $eh = set_error_handler(static function ($type, $msg, $file, $line) use ($name, &$deprecated, &$eh) { + if (\E_DEPRECATED === $type && "Constant $name is deprecated" === $msg) { + return $deprecated = true; + } + + return $eh && $eh($type, $msg, $file, $line); + }); + + try { + $this->value = constant($name); + $this->deprecated = $deprecated; + } finally { + restore_error_handler(); + } + } + + public function getName(): string + { + return $this->name; + } + + public function getValue() + { + return $this->value; + } + + public function getNamespaceName(): string + { + if (false === $slashPos = strrpos($this->name, '\\')) { + return ''; + } + + return substr($this->name, 0, $slashPos); + } + + public function getShortName(): string + { + if (false === $slashPos = strrpos($this->name, '\\')) { + return $this->name; + } + + return substr($this->name, $slashPos + 1); + } + + public function isDeprecated(): bool + { + return $this->deprecated; + } + + public function __toString(): string + { + // A constant is persistent if provided by PHP itself rather than + // being defined by users. If we got here, we know that it *is* + // defined, so we just need to figure out if it is defined by the + // user or not + if (!self::$persistentConstants) { + $persistentConstants = get_defined_constants(true); + unset($persistentConstants['user']); + foreach ($persistentConstants as $constants) { + self::$persistentConstants += $constants; + } + } + $persistent = array_key_exists($this->name, self::$persistentConstants); + + // Can't match the inclusion of `no_file_cache` but the rest is + // possible to match + $result = 'Constant [ '; + if ($persistent || $this->deprecated) { + $result .= '<'; + if ($persistent) { + $result .= 'persistent'; + if ($this->deprecated) { + $result .= ', '; + } + } + if ($this->deprecated) { + $result .= 'deprecated'; + } + $result .= '> '; + } + // Cannot just use gettype() to match zend_zval_type_name() + if (is_object($this->value)) { + $result .= \PHP_VERSION_ID >= 80000 ? get_debug_type($this->value) : gettype($this->value); + } elseif (is_bool($this->value)) { + $result .= 'bool'; + } elseif (is_int($this->value)) { + $result .= 'int'; + } elseif (is_float($this->value)) { + $result .= 'float'; + } elseif (null === $this->value) { + $result .= 'null'; + } else { + $result .= gettype($this->value); + } + $result .= ' '; + $result .= $this->name; + $result .= ' ] { '; + if (is_array($this->value)) { + $result .= 'Array'; + } else { + // This will throw an exception if the value is an object that + // cannot be converted to string; that is expected and matches + // the behavior of zval_get_string_func() + $result .= (string) $this->value; + } + $result .= " }\n"; + + return $result; + } + + public function __sleep(): array + { + throw new Exception("Serialization of 'ReflectionConstant' is not allowed"); + } + + public function __wakeup(): void + { + throw new Exception("Unserialization of 'ReflectionConstant' is not allowed"); + } + } +} diff --git a/tests/Php84/ReflectionConstantTest.php b/tests/Php84/ReflectionConstantTest.php new file mode 100644 index 000000000..53e6b947d --- /dev/null +++ b/tests/Php84/ReflectionConstantTest.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Tests\Php84; + +use PHPUnit\Framework\TestCase; + +const EXAMPLE = 'Foo'; + +class ExampleNonStringable +{ + protected $value; + + public function __construct(string $value) + { + $this->value = $value; + } +} + +class ExampleStringable extends ExampleNonStringable +{ + public function __toString(): string + { + return 'ExampleStringable (value='.$this->value.')'; + } +} + +/** + * @author Daniel Scherzer + */ +class ReflectionConstantTest extends TestCase +{ + public function testMissingConstant() + { + $this->expectException(\ReflectionException::class); + $this->expectExceptionMessage('Constant "missing" does not exist'); + new \ReflectionConstant('missing'); + } + + public function testClassConstant() + { + $this->assertTrue(\defined('ReflectionFunction::IS_DEPRECATED')); + + $this->expectException(\ReflectionException::class); + $this->expectExceptionMessage('Constant "ReflectionClass::IS_DEPRECATED" does not exist'); + new \ReflectionConstant('ReflectionClass::IS_DEPRECATED'); + } + + public function testBuiltInConstant() + { + $constant = new \ReflectionConstant('E_ERROR'); + $this->assertSame('E_ERROR', $constant->name); + $this->assertSame('E_ERROR', $constant->getName()); + $this->assertSame('', $constant->getNamespaceName()); + $this->assertSame('E_ERROR', $constant->getShortName()); + $this->assertSame(\E_ERROR, $constant->getValue()); + $this->assertFalse($constant->isDeprecated()); + $this->assertSame("Constant [ int E_ERROR ] { 1 }\n", (string) $constant); + } + + public function testUserConstant() + { + \define('TESTING', 123); + + $constant = new \ReflectionConstant('TESTING'); + $this->assertSame('TESTING', $constant->name); + $this->assertSame('TESTING', $constant->getName()); + $this->assertSame('', $constant->getNamespaceName()); + $this->assertSame('TESTING', $constant->getShortName()); + $this->assertSame(TESTING, $constant->getValue()); + $this->assertFalse($constant->isDeprecated()); + $this->assertSame("Constant [ int TESTING ] { 123 }\n", (string) $constant); + } + + public function testNamespacedConstant() + { + $constant = new \ReflectionConstant(EXAMPLE::class); + $this->assertSame(EXAMPLE::class, $constant->name); + $this->assertSame(EXAMPLE::class, $constant->getName()); + $this->assertSame(__NAMESPACE__, $constant->getNamespaceName()); + $this->assertSame('EXAMPLE', $constant->getShortName()); + $this->assertSame(EXAMPLE, $constant->getValue()); + $this->assertFalse($constant->isDeprecated()); + $this->assertSame("Constant [ string Symfony\\Polyfill\\Tests\\Php84\\EXAMPLE ] { Foo }\n", (string) $constant); + } + + public function testDeprecated() + { + $constant = new \ReflectionConstant('MT_RAND_PHP'); + $this->assertSame('MT_RAND_PHP', $constant->name); + $this->assertSame('MT_RAND_PHP', $constant->getName()); + $this->assertSame('', $constant->getNamespaceName()); + $this->assertSame('MT_RAND_PHP', $constant->getShortName()); + $this->assertSame(1, $constant->getValue()); + if (\PHP_VERSION_ID >= 80300) { + $this->assertTrue($constant->isDeprecated()); + $this->assertSame("Constant [ int MT_RAND_PHP ] { 1 }\n", (string) $constant); + } else { + $this->assertFalse($constant->isDeprecated()); + $this->assertSame("Constant [ int MT_RAND_PHP ] { 1 }\n", (string) $constant); + } + } + + public function testNonStringable() + { + if (\PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Constants can only be objects since PHP 8.1'); + } + $value = new ExampleNonStringable('Testing'); + \define('NonStringable', $value); + + $constant = new \ReflectionConstant('NonStringable'); + + // No error version of expectException() + try { + (string) $constant; + $this->fail('Error should be thrown'); + } catch (\Error $e) { + $this->assertSame("Object of class Symfony\Polyfill\Tests\Php84\ExampleNonStringable could not be converted to string", $e->getMessage()); + } + } + + public function testStringable() + { + if (\PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Constants can only be objects since PHP 8.1'); + } + $value = new ExampleStringable('Testing'); + \define('IsStringable', $value); + + $constant = new \ReflectionConstant('IsStringable'); + + $this->assertSame("Constant [ Symfony\Polyfill\Tests\Php84\ExampleStringable IsStringable ] { ExampleStringable (value=Testing) }\n", (string) $constant); + } + + public function testSerialization() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage("Serialization of 'ReflectionConstant' is not allowed"); + + $r = new \ReflectionConstant('PHP_VERSION'); + serialize($r); + } + + public function testUnserialization() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage("Unserialization of 'ReflectionConstant' is not allowed"); + unserialize( + 'O:18:"ReflectionConstant":4:{s:4:"name";'. + "s:11:\"PHP_VERSION\";s:25:\"\0ReflectionConstant\0value\";s:6:\"8.3.19\";". + "s:30:\"\0ReflectionConstant\0deprecated\";b:0;". + "s:30:\"\0ReflectionConstant\0persistent\";b:1;}" + ); + } +}