From 610bba8d5d5c166291602ab6c37fd3b4db5f72b0 Mon Sep 17 00:00:00 2001 From: Daniel Scherzer Date: Wed, 14 May 2025 10:26:41 -0400 Subject: [PATCH] [8.4] Add polyfill for `ReflectionConstant` --- README.md | 1 + src/Php84/README.md | 1 + .../Resources/stubs/ReflectionConstant.php | 167 ++++++++++ tests/Php84/ReflectionConstantTest.php | 292 ++++++++++++++++++ 4 files changed, 461 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..55daac32c --- /dev/null +++ b/src/Php84/Resources/stubs/ReflectionConstant.php @@ -0,0 +1,167 @@ + + * + * 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 { + // 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..b7620333d --- /dev/null +++ b/tests/Php84/ReflectionConstantTest.php @@ -0,0 +1,292 @@ + + * + * 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 Error; +use Exception; +use PHPUnit\Framework\TestCase; +use ReflectionConstant; +use ReflectionException; + +// Used by the test +const EXAMPLE = 'Foo'; + +class ExampleNonStringable { + + /** @var string */ + 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' + ); + $r = new ReflectionConstant('missing'); + } + + public function testClassConstant() + { + // Confirm that this constant actually exists + $this->assertTrue(defined('ReflectionFunction::IS_DEPRECATED')); + + $this->expectException(ReflectionException::class); + $this->expectExceptionMessage( + 'Constant "ReflectionClass::IS_DEPRECATED" does not exist' + ); + // 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 (PHP_VERSION_ID >= 80300) { + $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 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 { + $str = (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;}" + ); + } + +}