From 942f64e6b6c73b3b528cb21b0ef9bc880fc422eb Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Sun, 4 May 2025 13:57:36 -0700 Subject: [PATCH] [8.4] Add polyfill for ReflectionConstant --- README.md | 1 + src/Php84/README.md | 1 + .../Resources/stubs/ReflectionConstant.php | 164 +++++++++++++ tests/Php84/ReflectionConstantTest.php | 229 ++++++++++++++++++ 4 files changed, 395 insertions(+) create mode 100644 src/Php84/Resources/stubs/ReflectionConstant.php create mode 100644 tests/Php84/ReflectionConstantTest.php diff --git a/README.md b/README.md index 06b757d72..e8eab8000 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 `get_error_handler` and `get_exception_handler` functions introduced in PHP 8.5; - the `NoDiscard` attribute introduced in PHP 8.5; diff --git a/src/Php84/README.md b/src/Php84/README.md index dc838a530..4f1143ad8 100644 --- a/src/Php84/README.md +++ b/src/Php84/README.md @@ -9,6 +9,7 @@ This component provides features added to PHP 8.4 core: - `CURL_HTTP_VERSION_3` and `CURL_HTTP_VERSION_3ONLY` constants - [`fpow`](https://wiki.php.net/rfc/raising_zero_to_power_of_negative_number) - [`mb_trim`, `mb_ltrim` and `mb_rtrim`](https://wiki.php.net/rfc/mb_trim) +- [`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..2406ed441 --- /dev/null +++ b/src/Php84/Resources/stubs/ReflectionConstant.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * @author Daniel Scherzer + */ +if (\PHP_VERSION_ID < 80400) { + final class ReflectionConstant { + + /** + * @var string + * @readonly + */ + public $name; + + private $value; + + /** @var bool */ + private $deprecated; + + /** @var bool */ + private $persistent; + + public function __construct( string $name ) + { + if ( !defined( $name ) ) { + throw new ReflectionException( "Constant \"$name\" does not exist" ); + } + // ReflectionConstant cannot be used for class constants; constants + // with 2 `:` or more in a row are prohibited via define() + if ( strpos( $name, '::' ) !== false ) { + throw new ReflectionException( "Constant \"$name\" does not exist" ); + } + $this->name = $name; + $deprecated = false; + $old = set_error_handler( + static function ( + int $errno, + string $errstr + ) use ( $name, &$deprecated ) { + if ( $errno === E_DEPRECATED + && $errstr === "Constant $name is deprecated" + ) { + $deprecated = true; + } + return true; + } + ); + $this->value = constant( $name ); + $this->deprecated = $deprecated; + set_error_handler( $old ); + + // 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 + $allConstants = get_defined_constants(true); + $userConstants = $allConstants['user'] ?? []; + $key = ltrim( $this->name, '\\' ); + $this->persistent = !array_key_exists( $key, $userConstants ); + } + + public function getName(): string + { + return ltrim( $this->name, '\\' ); + } + + public function getValue() + { + return $this->value; + } + + public function getNamespaceName(): string + { + $slashPos = strrpos( $this->name, '\\' ); + if ( $slashPos === false ) { + return ''; + } + return substr( $this->name, 1, $slashPos - 1 ); + } + + public function getShortName(): string + { + $slashPos = strrpos( $this->name, '\\' ); + if ( $slashPos === false ) { + return $this->name; + } + return substr( $this->name, $slashPos + 1 ); + } + + public function isDeprecated(): bool + { + return $this->deprecated; + } + + public function __toString(): string + { + // Can't match the inclusion of `no_file_cache` but the rest is + // possible to match + $result = 'Constant [ '; + if ( $this->persistent || $this->deprecated ) { + $result .= '<'; + if ( $this->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 .= get_class( $this->value ); + } elseif ( is_bool( $this->value ) ) { + $result .= 'bool'; + } elseif ( is_int( $this->value ) ) { + $result .= 'int'; + } elseif ( is_float( $this->value ) ) { + $result .= 'float'; + } elseif ( is_string( $this->value ) ) { + $result .= 'string'; + } elseif ( is_resource( $this->value ) ) { + $result .= 'resource'; + } elseif ( $this->value === null ) { + $result .= 'null'; + } else { + // Reasonable fallback + $result .= gettype( $this->value ); + } + $result .= ' '; + $result .= $this->getName(); + $result .= ' ] { '; + if ( is_array( $this->value ) ) { + $result .= 'Array'; + } else { + $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..1b68d36f9 --- /dev/null +++ b/tests/Php84/ReflectionConstantTest.php @@ -0,0 +1,229 @@ + + * + * 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 Exception; +use PHPUnit\Framework\TestCase; +use ReflectionConstant; +use ReflectionException; + +// Used by the test +const EXAMPLE = 'Foo'; + +/** + * @author Daniel Scherzer + */ +class ReflectionConstantTest extends TestCase +{ + + public function testMissingConstant() + { + $this->expectException( ReflectionException::class ); + $this->expectExceptionMessage( + 'Constant "missing" does not exist' + ); + $r = new ReflectionConstant( 'missing' ); + } + + public function testClassConstant() + { + $this->expectException( ReflectionException::class ); + $this->expectExceptionMessage( + 'Constant "ReflectionClass::IS_DEPRECATED" does not exist' + ); + // Confirm that this constant actually exists + $this->assertTrue( defined( 'ReflectionFunction::IS_DEPRECATED' ) ); + // But ReflectionConstant doesn't support it + $r = new ReflectionConstant( 'ReflectionClass::IS_DEPRECATED' ); + } + + public function testBuiltInConstant() + { + $constant = new ReflectionConstant( 'E_ERROR' ); + $this->assertSame( 'E_ERROR', $constant->name, 'Name (from property)' ); + $this->assertSame( + 'E_ERROR', + $constant->getName(), + 'Name (from getter)' + ); + $this->assertSame( + '', + $constant->getNamespaceName(), + 'No namespace' + ); + $this->assertSame( + 'E_ERROR', + $constant->getShortName(), + 'Short name' + ); + $this->assertSame( + E_ERROR, + $constant->getValue(), + 'Value' + ); + $this->assertFalse( + $constant->isDeprecated(), + 'Not deprecated' + ); + $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, 'Name (from property)' ); + $this->assertSame( + 'TESTING', + $constant->getName(), + 'Name (from getter)' + ); + $this->assertSame( + '', + $constant->getNamespaceName(), + 'No namespace' + ); + $this->assertSame( + 'TESTING', + $constant->getShortName(), + 'Short name' + ); + $this->assertSame( + TESTING, + $constant->getValue(), + 'Value' + ); + $this->assertFalse( + $constant->isDeprecated(), + 'Not deprecated' + ); + $this->assertSame( + "Constant [ int TESTING ] { 123 }\n", + (string)$constant + ); + } + + public function testNamespacedConstant() + { + $constant = new ReflectionConstant( + '\\Symfony\\Polyfill\\Tests\\Php84\\EXAMPLE' + ); + $this->assertSame( + '\\Symfony\\Polyfill\\Tests\\Php84\\EXAMPLE', + $constant->name, + 'Name (from property)' + ); + $this->assertSame( + 'Symfony\\Polyfill\\Tests\\Php84\\EXAMPLE', + $constant->getName(), + 'Name (from getter)' + ); + $this->assertSame( + 'Symfony\\Polyfill\\Tests\\Php84', + $constant->getNamespaceName(), + 'Has namespace' + ); + $this->assertSame( + 'EXAMPLE', + $constant->getShortName(), + 'Short name' + ); + $this->assertSame( + EXAMPLE, + $constant->getValue(), + 'Value' + ); + $this->assertFalse( + $constant->isDeprecated(), + 'Not deprecated' + ); + $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, 'Name (from property)' ); + $this->assertSame( + 'MT_RAND_PHP', + $constant->getName(), + 'Name (from getter)' + ); + $this->assertSame( + '', + $constant->getNamespaceName(), + 'No namespace' + ); + $this->assertSame( + 'MT_RAND_PHP', + $constant->getShortName(), + 'Short name' + ); + $this->assertSame( + 1, + $constant->getValue(), + 'Value' + ); + if ( version_compare( PHP_VERSION, '8.3.0', '>=' ) ) { + $this->assertTrue( + $constant->isDeprecated(), + 'Deprecated since 8.3' + ); + $this->assertSame( + "Constant [ int MT_RAND_PHP ] { 1 }\n", + (string)$constant + ); + } else { + $this->assertFalse( + $constant->isDeprecated(), + 'Not deprecated until 8.3' + ); + $this->assertSame( + "Constant [ int MT_RAND_PHP ] { 1 }\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;}" + ); + } + +}