Skip to content

Commit c4a2ffa

Browse files
DanielEScherzernicolas-grekas
authored andcommitted
[8.4] Add polyfill for ReflectionConstant
1 parent 1f819a2 commit c4a2ffa

File tree

5 files changed

+325
-1
lines changed

5 files changed

+325
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Polyfills are provided for:
7171
- the `array_find`, `array_find_key`, `array_any` and `array_all` functions introduced in PHP 8.4;
7272
- the `Deprecated` attribute introduced in PHP 8.4;
7373
- the `mb_trim`, `mb_ltrim` and `mb_rtrim` functions introduced in PHP 8.4;
74+
- the `ReflectionConstant` class introduced in PHP 8.4
7475
- the `CURL_HTTP_VERSION_3` and `CURL_HTTP_VERSION_3ONLY` constants introduced in PHP 8.4;
7576
- the `grapheme_str_split` function introduced in PHP 8.4;
7677
- the `bcdivmod` function introduced in PHP 8.4;

src/Php84/Php84.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public static function bcdivmod(string $num1, string $num2, ?int $scale = null):
210210
if (null === $quot = \bcdiv($num1, $num2, 0)) {
211211
return null;
212212
}
213-
$scale = $scale ?? (\PHP_VERSION_ID >= 70300 ? \bcscale() : (ini_get('bcmath.scale') ?: 0);
213+
$scale = $scale ?? (\PHP_VERSION_ID >= 70300 ? \bcscale() : (ini_get('bcmath.scale') ?: 0));
214214

215215
return [$quot, \bcmod($num1, $num2, $scale)];
216216
}

src/Php84/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ This component provides features added to PHP 8.4 core:
1111
- [`grapheme_str_split`](https://wiki.php.net/rfc/grapheme_str_split)
1212
- [`mb_trim`, `mb_ltrim` and `mb_rtrim`](https://wiki.php.net/rfc/mb_trim)
1313
- [`mb_ucfirst` and `mb_lcfirst`](https://wiki.php.net/rfc/mb_ucfirst)
14+
- [`ReflectionConstant`](https://github.com/php/php-src/pull/13669)
1415

1516
More information can be found in the
1617
[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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+
/**
14+
* @author Daniel Scherzer <[email protected]>
15+
*/
16+
final class ReflectionConstant
17+
{
18+
/**
19+
* @var string
20+
*
21+
* @readonly
22+
*/
23+
public $name;
24+
25+
private $value;
26+
private $deprecated;
27+
28+
private static $persistentConstants = [];
29+
30+
public function __construct(string $name)
31+
{
32+
if (!defined($name) || false !== strpos($name, '::')) {
33+
throw new ReflectionException("Constant \"$name\" does not exist");
34+
}
35+
36+
$this->name = ltrim($name, '\\');
37+
$deprecated = false;
38+
$eh = set_error_handler(static function ($type, $msg, $file, $line) use ($name, &$deprecated, &$eh) {
39+
if (\E_DEPRECATED === $type && "Constant $name is deprecated" === $msg) {
40+
return $deprecated = true;
41+
}
42+
43+
return $eh && $eh($type, $msg, $file, $line);
44+
});
45+
46+
try {
47+
$this->value = constant($name);
48+
$this->deprecated = $deprecated;
49+
} finally {
50+
restore_error_handler();
51+
}
52+
}
53+
54+
public function getName(): string
55+
{
56+
return $this->name;
57+
}
58+
59+
public function getValue()
60+
{
61+
return $this->value;
62+
}
63+
64+
public function getNamespaceName(): string
65+
{
66+
if (false === $slashPos = strrpos($this->name, '\\')) {
67+
return '';
68+
}
69+
70+
return substr($this->name, 0, $slashPos);
71+
}
72+
73+
public function getShortName(): string
74+
{
75+
if (false === $slashPos = strrpos($this->name, '\\')) {
76+
return $this->name;
77+
}
78+
79+
return substr($this->name, $slashPos + 1);
80+
}
81+
82+
public function isDeprecated(): bool
83+
{
84+
return $this->deprecated;
85+
}
86+
87+
public function __toString(): string
88+
{
89+
// A constant is persistent if provided by PHP itself rather than
90+
// being defined by users. If we got here, we know that it *is*
91+
// defined, so we just need to figure out if it is defined by the
92+
// user or not
93+
if (!self::$persistentConstants) {
94+
$persistentConstants = get_defined_constants(true);
95+
unset($persistentConstants['user']);
96+
foreach ($persistentConstants as $constants) {
97+
self::$persistentConstants += $constants;
98+
}
99+
}
100+
$persistent = array_key_exists($this->name, self::$persistentConstants);
101+
102+
// Can't match the inclusion of `no_file_cache` but the rest is
103+
// possible to match
104+
$result = 'Constant [ ';
105+
if ($persistent || $this->deprecated) {
106+
$result .= '<';
107+
if ($persistent) {
108+
$result .= 'persistent';
109+
if ($this->deprecated) {
110+
$result .= ', ';
111+
}
112+
}
113+
if ($this->deprecated) {
114+
$result .= 'deprecated';
115+
}
116+
$result .= '> ';
117+
}
118+
// Cannot just use gettype() to match zend_zval_type_name()
119+
if (is_object($this->value)) {
120+
$result .= \PHP_VERSION_ID >= 80000 ? get_debug_type($this->value) : gettype($this->value);
121+
} elseif (is_bool($this->value)) {
122+
$result .= 'bool';
123+
} elseif (is_int($this->value)) {
124+
$result .= 'int';
125+
} elseif (is_float($this->value)) {
126+
$result .= 'float';
127+
} elseif (null === $this->value) {
128+
$result .= 'null';
129+
} else {
130+
$result .= gettype($this->value);
131+
}
132+
$result .= ' ';
133+
$result .= $this->name;
134+
$result .= ' ] { ';
135+
if (is_array($this->value)) {
136+
$result .= 'Array';
137+
} else {
138+
// This will throw an exception if the value is an object that
139+
// cannot be converted to string; that is expected and matches
140+
// the behavior of zval_get_string_func()
141+
$result .= (string) $this->value;
142+
}
143+
$result .= " }\n";
144+
145+
return $result;
146+
}
147+
148+
public function __sleep(): array
149+
{
150+
throw new Exception("Serialization of 'ReflectionConstant' is not allowed");
151+
}
152+
153+
public function __wakeup(): void
154+
{
155+
throw new Exception("Unserialization of 'ReflectionConstant' is not allowed");
156+
}
157+
}
158+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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\Tests\Php84;
13+
14+
use PHPUnit\Framework\TestCase;
15+
16+
const EXAMPLE = 'Foo';
17+
18+
class ExampleNonStringable
19+
{
20+
protected $value;
21+
22+
public function __construct(string $value)
23+
{
24+
$this->value = $value;
25+
}
26+
}
27+
28+
class ExampleStringable extends ExampleNonStringable
29+
{
30+
public function __toString(): string
31+
{
32+
return 'ExampleStringable (value='.$this->value.')';
33+
}
34+
}
35+
36+
/**
37+
* @author Daniel Scherzer <[email protected]>
38+
*/
39+
class ReflectionConstantTest extends TestCase
40+
{
41+
public function testMissingConstant()
42+
{
43+
$this->expectException(\ReflectionException::class);
44+
$this->expectExceptionMessage('Constant "missing" does not exist');
45+
new \ReflectionConstant('missing');
46+
}
47+
48+
public function testClassConstant()
49+
{
50+
$this->assertTrue(\defined('ReflectionFunction::IS_DEPRECATED'));
51+
52+
$this->expectException(\ReflectionException::class);
53+
$this->expectExceptionMessage('Constant "ReflectionClass::IS_DEPRECATED" does not exist');
54+
new \ReflectionConstant('ReflectionClass::IS_DEPRECATED');
55+
}
56+
57+
public function testBuiltInConstant()
58+
{
59+
$constant = new \ReflectionConstant('E_ERROR');
60+
$this->assertSame('E_ERROR', $constant->name);
61+
$this->assertSame('E_ERROR', $constant->getName());
62+
$this->assertSame('', $constant->getNamespaceName());
63+
$this->assertSame('E_ERROR', $constant->getShortName());
64+
$this->assertSame(\E_ERROR, $constant->getValue());
65+
$this->assertFalse($constant->isDeprecated());
66+
$this->assertSame("Constant [ <persistent> int E_ERROR ] { 1 }\n", (string) $constant);
67+
}
68+
69+
public function testUserConstant()
70+
{
71+
\define('TESTING', 123);
72+
73+
$constant = new \ReflectionConstant('TESTING');
74+
$this->assertSame('TESTING', $constant->name);
75+
$this->assertSame('TESTING', $constant->getName());
76+
$this->assertSame('', $constant->getNamespaceName());
77+
$this->assertSame('TESTING', $constant->getShortName());
78+
$this->assertSame(TESTING, $constant->getValue());
79+
$this->assertFalse($constant->isDeprecated());
80+
$this->assertSame("Constant [ int TESTING ] { 123 }\n", (string) $constant);
81+
}
82+
83+
public function testNamespacedConstant()
84+
{
85+
$constant = new \ReflectionConstant(EXAMPLE::class);
86+
$this->assertSame(EXAMPLE::class, $constant->name);
87+
$this->assertSame(EXAMPLE::class, $constant->getName());
88+
$this->assertSame(__NAMESPACE__, $constant->getNamespaceName());
89+
$this->assertSame('EXAMPLE', $constant->getShortName());
90+
$this->assertSame(EXAMPLE, $constant->getValue());
91+
$this->assertFalse($constant->isDeprecated());
92+
$this->assertSame("Constant [ string Symfony\\Polyfill\\Tests\\Php84\\EXAMPLE ] { Foo }\n", (string) $constant);
93+
}
94+
95+
public function testDeprecated()
96+
{
97+
$constant = new \ReflectionConstant('MT_RAND_PHP');
98+
$this->assertSame('MT_RAND_PHP', $constant->name);
99+
$this->assertSame('MT_RAND_PHP', $constant->getName());
100+
$this->assertSame('', $constant->getNamespaceName());
101+
$this->assertSame('MT_RAND_PHP', $constant->getShortName());
102+
$this->assertSame(1, $constant->getValue());
103+
if (\PHP_VERSION_ID >= 80300) {
104+
$this->assertTrue($constant->isDeprecated());
105+
$this->assertSame("Constant [ <persistent, deprecated> int MT_RAND_PHP ] { 1 }\n", (string) $constant);
106+
} else {
107+
$this->assertFalse($constant->isDeprecated());
108+
$this->assertSame("Constant [ <persistent> int MT_RAND_PHP ] { 1 }\n", (string) $constant);
109+
}
110+
}
111+
112+
public function testNonStringable()
113+
{
114+
if (\PHP_VERSION_ID < 80100) {
115+
$this->markTestSkipped('Constants can only be objects since PHP 8.1');
116+
}
117+
$value = new ExampleNonStringable('Testing');
118+
\define('NonStringable', $value);
119+
120+
$constant = new \ReflectionConstant('NonStringable');
121+
122+
// No error version of expectException()
123+
try {
124+
(string) $constant;
125+
$this->fail('Error should be thrown');
126+
} catch (\Error $e) {
127+
$this->assertSame("Object of class Symfony\Polyfill\Tests\Php84\ExampleNonStringable could not be converted to string", $e->getMessage());
128+
}
129+
}
130+
131+
public function testStringable()
132+
{
133+
if (\PHP_VERSION_ID < 80100) {
134+
$this->markTestSkipped('Constants can only be objects since PHP 8.1');
135+
}
136+
$value = new ExampleStringable('Testing');
137+
\define('IsStringable', $value);
138+
139+
$constant = new \ReflectionConstant('IsStringable');
140+
141+
$this->assertSame("Constant [ Symfony\Polyfill\Tests\Php84\ExampleStringable IsStringable ] { ExampleStringable (value=Testing) }\n", (string) $constant);
142+
}
143+
144+
public function testSerialization()
145+
{
146+
$this->expectException(\Exception::class);
147+
$this->expectExceptionMessage("Serialization of 'ReflectionConstant' is not allowed");
148+
149+
$r = new \ReflectionConstant('PHP_VERSION');
150+
serialize($r);
151+
}
152+
153+
public function testUnserialization()
154+
{
155+
$this->expectException(\Exception::class);
156+
$this->expectExceptionMessage("Unserialization of 'ReflectionConstant' is not allowed");
157+
unserialize(
158+
'O:18:"ReflectionConstant":4:{s:4:"name";'.
159+
"s:11:\"PHP_VERSION\";s:25:\"\0ReflectionConstant\0value\";s:6:\"8.3.19\";".
160+
"s:30:\"\0ReflectionConstant\0deprecated\";b:0;".
161+
"s:30:\"\0ReflectionConstant\0persistent\";b:1;}"
162+
);
163+
}
164+
}

0 commit comments

Comments
 (0)