parent
952210f1ff
commit
6baaa9a221
15 changed files with 701 additions and 1 deletions
@ -0,0 +1,26 @@ |
|||||||
|
--TEST-- |
||||||
|
object link operator |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
function main() |
||||||
|
{ |
||||||
|
$a = 1; |
||||||
|
$b = &$a; |
||||||
|
$b = 2; |
||||||
|
var_dump($a, $b); |
||||||
|
unset($b); |
||||||
|
var_dump($a); |
||||||
|
var_dump(isset($b)); |
||||||
|
// var_dump($b ?? 123); // 修复前这个报错 |
||||||
|
|
||||||
|
$b = 1; |
||||||
|
var_dump($b, $a); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(2) |
||||||
|
int(2) |
||||||
|
int(2) |
||||||
|
bool(false) |
||||||
|
int(1) |
||||||
|
int(2) |
||||||
@ -0,0 +1,43 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: Closure::bind returns reference to private property |
||||||
|
--SKIPIF-- |
||||||
|
<?php |
||||||
|
exit("skip: returning by reference from a closure is not supported in AOT"); |
||||||
|
?> |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class ReferenceConfigurator |
||||||
|
{ |
||||||
|
private array $instanceof = [ |
||||||
|
'base' => 'service', |
||||||
|
]; |
||||||
|
|
||||||
|
public function getReferenceAccessor(): Closure |
||||||
|
{ |
||||||
|
return Closure::bind(fn &() => $this->instanceof, $this, self::class); |
||||||
|
} |
||||||
|
|
||||||
|
public function dump(): void |
||||||
|
{ |
||||||
|
foreach ($this->instanceof as $key => $value) { |
||||||
|
var_dump($key.':'.$value); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$configurator = new ReferenceConfigurator(); |
||||||
|
$accessor = $configurator->getReferenceAccessor(); |
||||||
|
$instanceof = &$accessor(); |
||||||
|
|
||||||
|
$instanceof['extra'] = 'listener'; |
||||||
|
$instanceof['base'] = strtoupper($instanceof['base']); |
||||||
|
|
||||||
|
$configurator->dump(); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(12) "base:SERVICE" |
||||||
|
string(14) "extra:listener" |
||||||
@ -0,0 +1,57 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: __call forwards to concatenated dynamic setter with unpack |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class Configurator |
||||||
|
{ |
||||||
|
private array $values = []; |
||||||
|
|
||||||
|
public function __call(string $method, array $args): mixed |
||||||
|
{ |
||||||
|
if (method_exists($this, 'set'.$method)) { |
||||||
|
return $this->{'set'.$method}(...$args); |
||||||
|
} |
||||||
|
|
||||||
|
throw new BadMethodCallException(sprintf('Call to undefined method "%s::%s()".', static::class, $method)); |
||||||
|
} |
||||||
|
|
||||||
|
private function setOption(string $name, mixed $value, bool $append = false): static |
||||||
|
{ |
||||||
|
if ($append) { |
||||||
|
$this->values[$name][] = $value; |
||||||
|
} else { |
||||||
|
$this->values[$name] = $value; |
||||||
|
} |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function all(): array |
||||||
|
{ |
||||||
|
return $this->values; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$configurator = new Configurator(); |
||||||
|
$configurator->Option('debug', true); |
||||||
|
$configurator->Option('tags', 'console', true); |
||||||
|
$configurator->Option(...['tags', 'worker', true]); |
||||||
|
|
||||||
|
var_dump($configurator->all()); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(2) { |
||||||
|
["debug"]=> |
||||||
|
bool(true) |
||||||
|
["tags"]=> |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(7) "console" |
||||||
|
[1]=> |
||||||
|
string(6) "worker" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,66 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: dynamic registry property with array coalesce assignment |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class RegistryContainer |
||||||
|
{ |
||||||
|
public array $services = []; |
||||||
|
public array $privates = []; |
||||||
|
public array $factories = []; |
||||||
|
|
||||||
|
public function getService(string|false $registry, string $id, ?string $method, string|bool $load): mixed |
||||||
|
{ |
||||||
|
if ('service_container' === $id) { |
||||||
|
return $this; |
||||||
|
} |
||||||
|
if (is_string($load)) { |
||||||
|
throw new RuntimeException($load); |
||||||
|
} |
||||||
|
if (null === $method) { |
||||||
|
return false !== $registry ? $this->{$registry}[$id] ?? null : null; |
||||||
|
} |
||||||
|
if (false !== $registry) { |
||||||
|
return $this->{$registry}[$id] ??= $load ? $this->load($method) : $this->{$method}($this); |
||||||
|
} |
||||||
|
if (!$load) { |
||||||
|
return $this->{$method}($this); |
||||||
|
} |
||||||
|
|
||||||
|
return ($factory = $this->factories[$id] ?? $this->factories['service_container'][$id] ?? null) ? $factory($this) : $this->load($method); |
||||||
|
} |
||||||
|
|
||||||
|
private function load(string $method): object |
||||||
|
{ |
||||||
|
return $this->{$method}($this); |
||||||
|
} |
||||||
|
|
||||||
|
private function createLogger(self $container): object |
||||||
|
{ |
||||||
|
return (object) ['id' => 'logger', 'count' => count($container->services) + count($container->privates)]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$container = new RegistryContainer(); |
||||||
|
$container->factories['service_container']['mailer'] = static fn (RegistryContainer $container): object => (object) ['id' => 'mailer']; |
||||||
|
|
||||||
|
$logger = $container->getService('services', 'logger', 'createLogger', false); |
||||||
|
$sameLogger = $container->getService('services', 'logger', 'createLogger', false); |
||||||
|
$privateLogger = $container->getService('privates', 'logger', 'createLogger', true); |
||||||
|
$mailer = $container->getService(false, 'mailer', 'createLogger', true); |
||||||
|
|
||||||
|
var_dump($logger === $sameLogger); |
||||||
|
var_dump($logger->id, $logger->count); |
||||||
|
var_dump($privateLogger->id, $privateLogger->count); |
||||||
|
var_dump($mailer->id); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
bool(true) |
||||||
|
string(6) "logger" |
||||||
|
int(0) |
||||||
|
string(6) "logger" |
||||||
|
int(1) |
||||||
|
string(6) "mailer" |
||||||
@ -0,0 +1,49 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: container fallback caches static first-class callable |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class MiniContainer |
||||||
|
{ |
||||||
|
public array $services = []; |
||||||
|
public array $aliases = []; |
||||||
|
public array $factories = []; |
||||||
|
public static mixed $make = null; |
||||||
|
|
||||||
|
public function __construct() |
||||||
|
{ |
||||||
|
$this->factories['custom'] = static fn (self $container): object => (object) ['id' => 'custom']; |
||||||
|
} |
||||||
|
|
||||||
|
public function get(string $id): object |
||||||
|
{ |
||||||
|
return $this->services[$id] |
||||||
|
?? $this->services[$id = $this->aliases[$id] ?? $id] |
||||||
|
?? ('service_container' === $id ? $this : ($this->factories[$id] ?? self::$make ??= self::make(...))($this, $id)); |
||||||
|
} |
||||||
|
|
||||||
|
public static function make(self $container, string $id): object |
||||||
|
{ |
||||||
|
$service = (object) ['id' => $id]; |
||||||
|
$container->services[$id] = $service; |
||||||
|
|
||||||
|
return $service; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$container = new MiniContainer(); |
||||||
|
$container->aliases['logger'] = 'monolog'; |
||||||
|
|
||||||
|
var_dump($container->get('logger')->id); |
||||||
|
var_dump($container->get('monolog')->id); |
||||||
|
var_dump($container->get('custom')->id); |
||||||
|
var_dump(MiniContainer::$make instanceof Closure); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(7) "monolog" |
||||||
|
string(7) "monolog" |
||||||
|
string(6) "custom" |
||||||
|
bool(true) |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: dynamic first-class callable branch |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class CallableTarget |
||||||
|
{ |
||||||
|
public function format(string $value): string |
||||||
|
{ |
||||||
|
return 'object:'.strtoupper($value); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function format_global(string $value): string |
||||||
|
{ |
||||||
|
return 'function:'.strtolower($value); |
||||||
|
} |
||||||
|
|
||||||
|
function makeCallable(array $callable): Closure |
||||||
|
{ |
||||||
|
return $callable[0] ? $callable[0]->{$callable[1]}(...) : $callable[1](...); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$objectCallable = makeCallable([new CallableTarget(), 'format']); |
||||||
|
$functionCallable = makeCallable([null, 'format_global']); |
||||||
|
|
||||||
|
var_dump($objectCallable('symfony')); |
||||||
|
var_dump($functionCallable('AOT')); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(14) "object:SYMFONY" |
||||||
|
string(12) "function:aot" |
||||||
@ -0,0 +1,56 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: lazy string resolves array callable with ??= method name |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class Formatter |
||||||
|
{ |
||||||
|
public function __invoke(string $value, string $prefix = ''): string |
||||||
|
{ |
||||||
|
return $prefix.strtoupper($value); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class LazyString |
||||||
|
{ |
||||||
|
public mixed $value; |
||||||
|
|
||||||
|
public static function fromCallable(array|callable $callback, array $arguments): self |
||||||
|
{ |
||||||
|
$lazyString = new self(); |
||||||
|
$lazyString->value = static function () use (&$callback, &$arguments): string { |
||||||
|
static $value; |
||||||
|
|
||||||
|
if (null !== $arguments) { |
||||||
|
if (!is_callable($callback)) { |
||||||
|
$callback[0] = $callback[0](); |
||||||
|
$callback[1] ??= '__invoke'; |
||||||
|
} |
||||||
|
$value = $callback(...$arguments); |
||||||
|
$callback = 'callable'; |
||||||
|
$arguments = null; |
||||||
|
} |
||||||
|
|
||||||
|
return $value; |
||||||
|
}; |
||||||
|
|
||||||
|
return $lazyString; |
||||||
|
} |
||||||
|
|
||||||
|
public function __toString(): string |
||||||
|
{ |
||||||
|
return ($this->value)(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$lazy = LazyString::fromCallable([static fn () => new Formatter()], ['symfony', 'app:']); |
||||||
|
|
||||||
|
var_dump((string) $lazy); |
||||||
|
var_dump((string) $lazy); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(11) "app:SYMFONY" |
||||||
|
string(11) "app:SYMFONY" |
||||||
@ -0,0 +1,54 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: get_object_vars parameters and array_key_last tail output |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class RouteParam |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
public string $slug, |
||||||
|
public int $page = 1, |
||||||
|
private string $internal = 'hidden', |
||||||
|
) { |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function mergeRouteParams(array $defaults, array $parameters): array |
||||||
|
{ |
||||||
|
foreach ($parameters as $key => $value) { |
||||||
|
if (is_object($value) && $vars = get_object_vars($value)) { |
||||||
|
unset($parameters[$key]); |
||||||
|
$parameters += $vars; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $parameters + $defaults; |
||||||
|
} |
||||||
|
|
||||||
|
function streamJsonParts(array $jsonParts): void |
||||||
|
{ |
||||||
|
echo $jsonParts[array_key_last($jsonParts)]; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(mergeRouteParams(['_route' => 'blog_show', 'page' => 1], [ |
||||||
|
'post' => new RouteParam('symfony-aot', 3), |
||||||
|
'format' => 'json', |
||||||
|
])); |
||||||
|
|
||||||
|
streamJsonParts(['{"items":', '[1,2,3]', '}']); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(4) { |
||||||
|
["format"]=> |
||||||
|
string(4) "json" |
||||||
|
["slug"]=> |
||||||
|
string(11) "symfony-aot" |
||||||
|
["page"]=> |
||||||
|
int(3) |
||||||
|
["_route"]=> |
||||||
|
string(9) "blog_show" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,58 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: __serialize and __unserialize restore private state |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class SerializableState |
||||||
|
{ |
||||||
|
private string $resource; |
||||||
|
private ?float $expiresAt = null; |
||||||
|
private array $state = []; |
||||||
|
|
||||||
|
public function __construct(string $resource) |
||||||
|
{ |
||||||
|
$this->resource = $resource; |
||||||
|
} |
||||||
|
|
||||||
|
public function setState(string $store, mixed $value): void |
||||||
|
{ |
||||||
|
$this->state[$store] = $value; |
||||||
|
} |
||||||
|
|
||||||
|
public function __serialize(): array |
||||||
|
{ |
||||||
|
return [ |
||||||
|
'resource' => $this->resource, |
||||||
|
'expiresAt' => $this->expiresAt, |
||||||
|
'state' => $this->state, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
public function __unserialize(array $data): void |
||||||
|
{ |
||||||
|
$this->resource = $data['resource']; |
||||||
|
$this->expiresAt = $data['expiresAt'] ?? null; |
||||||
|
$this->state = $data['state'] ?? []; |
||||||
|
} |
||||||
|
|
||||||
|
public function describe(): string |
||||||
|
{ |
||||||
|
return $this->resource.':'.implode(',', array_keys($this->state)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$state = new SerializableState('lock-key'); |
||||||
|
$state->setState('redis', ['token' => 'abc']); |
||||||
|
$state->setState('pdo', ['token' => 'def']); |
||||||
|
|
||||||
|
$copy = unserialize(serialize($state)); |
||||||
|
|
||||||
|
var_dump($copy instanceof SerializableState); |
||||||
|
var_dump($copy->describe()); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
bool(true) |
||||||
|
string(18) "lock-key:redis,pdo" |
||||||
@ -0,0 +1,29 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: binary string offset mutation with ord/chr carry |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class BinaryUtil |
||||||
|
{ |
||||||
|
public static function add(string $a, string $b): string |
||||||
|
{ |
||||||
|
$carry = 0; |
||||||
|
for ($i = 7; 0 <= $i; --$i) { |
||||||
|
$carry += ord($a[$i]) + ord($b[$i]); |
||||||
|
$a[$i] = chr($carry & 0xFF); |
||||||
|
$carry >>= 8; |
||||||
|
} |
||||||
|
|
||||||
|
return $a; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(bin2hex(BinaryUtil::add(hex2bin('00000000000000ff'), hex2bin('0000000000000001')))); |
||||||
|
var_dump(bin2hex(BinaryUtil::add(hex2bin('ffffffffffffffff'), hex2bin('0000000000000001')))); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(16) "0000000000000100" |
||||||
|
string(16) "0000000000000000" |
||||||
@ -0,0 +1,61 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: first-class builtin array_map and variadic context merge |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class AttributeMetadata |
||||||
|
{ |
||||||
|
private array $normalizationContexts = []; |
||||||
|
|
||||||
|
public function setNormalizationContextForGroups(array $context, array $groups = []): void |
||||||
|
{ |
||||||
|
if (!$groups) { |
||||||
|
$this->normalizationContexts['*'] = $context; |
||||||
|
} |
||||||
|
|
||||||
|
foreach ($groups as $group) { |
||||||
|
$this->normalizationContexts[$group] = $context; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public function getNormalizationContextForGroups(array $groups): array |
||||||
|
{ |
||||||
|
$contexts = []; |
||||||
|
foreach ($groups as $group) { |
||||||
|
$contexts[] = $this->normalizationContexts[$group] ?? []; |
||||||
|
} |
||||||
|
|
||||||
|
return array_merge($this->normalizationContexts['*'] ?? [], ...$contexts); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function countWords(array $words): int |
||||||
|
{ |
||||||
|
return count(array_filter(array_map(trim(...), $words), static fn ($word) => '' !== $word)); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(countWords([' one ', '', " \t ", 'two', ' three '])); |
||||||
|
|
||||||
|
$metadata = new AttributeMetadata(); |
||||||
|
$metadata->setNormalizationContextForGroups(['skip_null' => true]); |
||||||
|
$metadata->setNormalizationContextForGroups(['groups' => ['public']], ['read']); |
||||||
|
$metadata->setNormalizationContextForGroups(['max_depth' => 2], ['detail']); |
||||||
|
|
||||||
|
var_dump($metadata->getNormalizationContextForGroups(['read', 'missing', 'detail'])); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(3) |
||||||
|
array(3) { |
||||||
|
["skip_null"]=> |
||||||
|
bool(true) |
||||||
|
["groups"]=> |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
string(6) "public" |
||||||
|
} |
||||||
|
["max_depth"]=> |
||||||
|
int(2) |
||||||
|
} |
||||||
@ -0,0 +1,49 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: clone object from static cache with null coalescing |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class CloneRegistry |
||||||
|
{ |
||||||
|
public static array $prototypes = []; |
||||||
|
|
||||||
|
public static function create(string $class): object |
||||||
|
{ |
||||||
|
echo "create:$class\n"; |
||||||
|
return new $class('created'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class CloneableService |
||||||
|
{ |
||||||
|
public function __construct(public string $name) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getCachedPrototype(string $class): object |
||||||
|
{ |
||||||
|
return clone (CloneRegistry::$prototypes[$class] ?? CloneRegistry::create($class)); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$class = CloneableService::class; |
||||||
|
|
||||||
|
$first = getCachedPrototype($class); |
||||||
|
CloneRegistry::$prototypes[$class] = new CloneableService('cached'); |
||||||
|
$second = getCachedPrototype($class); |
||||||
|
|
||||||
|
$first->name = 'mutated'; |
||||||
|
$second->name = 'changed'; |
||||||
|
|
||||||
|
var_dump($first->name); |
||||||
|
var_dump($second->name); |
||||||
|
var_dump(CloneRegistry::$prototypes[$class]->name); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
create:CloneableService |
||||||
|
string(7) "mutated" |
||||||
|
string(7) "changed" |
||||||
|
string(6) "cached" |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: reference cache with clone and null coalescing |
||||||
|
--SKIPIF-- |
||||||
|
<?php |
||||||
|
exit("skip: assigning a reference from a complex static property expression is not supported in AOT"); |
||||||
|
?> |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
final class PrototypeRegistry |
||||||
|
{ |
||||||
|
public static array $prototypes = []; |
||||||
|
|
||||||
|
public static function create(string $class): object |
||||||
|
{ |
||||||
|
echo "create:$class\n"; |
||||||
|
return new $class('created'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class ExportedService |
||||||
|
{ |
||||||
|
public function __construct(public string $name) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getPrototype(string $class): object |
||||||
|
{ |
||||||
|
return clone (($p = &PrototypeRegistry::$prototypes)[$class] ?? PrototypeRegistry::create($class)); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$class = ExportedService::class; |
||||||
|
|
||||||
|
$first = getPrototype($class); |
||||||
|
PrototypeRegistry::$prototypes[$class] = new ExportedService('cached'); |
||||||
|
$second = getPrototype($class); |
||||||
|
|
||||||
|
$first->name = 'mutated'; |
||||||
|
$second->name = 'changed'; |
||||||
|
|
||||||
|
var_dump($first->name); |
||||||
|
var_dump($second->name); |
||||||
|
var_dump(PrototypeRegistry::$prototypes[$class]->name); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
create:ExportedService |
||||||
|
string(7) "mutated" |
||||||
|
string(7) "changed" |
||||||
|
string(6) "cached" |
||||||
@ -0,0 +1,63 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony pattern: static property default cache keyed by runtime class |
||||||
|
--XFAIL-- |
||||||
|
Known AOT bug: coalesce assignment with a ternary right-hand expression can cache the condition result instead of the full ternary value. |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class StubLike |
||||||
|
{ |
||||||
|
private static array $propertyDefaults = []; |
||||||
|
|
||||||
|
public string $name = 'default'; |
||||||
|
public ?int $count; |
||||||
|
public mixed $extra = null; |
||||||
|
|
||||||
|
public function __construct(string $name, ?int $count, mixed $extra = null) |
||||||
|
{ |
||||||
|
$this->name = $name; |
||||||
|
$this->count = $count; |
||||||
|
$this->extra = $extra; |
||||||
|
} |
||||||
|
|
||||||
|
public function __serialize(): array |
||||||
|
{ |
||||||
|
static $noDefault = new stdClass(); |
||||||
|
|
||||||
|
$data = []; |
||||||
|
foreach ($this as $k => $v) { |
||||||
|
$default = self::$propertyDefaults[$this::class][$k] ??= ($p = new ReflectionProperty($this, $k))->hasDefaultValue() |
||||||
|
? $p->getDefaultValue() |
||||||
|
: ($p->hasType() ? $noDefault : null); |
||||||
|
|
||||||
|
if ($noDefault === $default || $default !== $v) { |
||||||
|
$data[$k] = $v; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $data; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump((new StubLike('default', null))->__serialize()); |
||||||
|
var_dump((new StubLike('changed', 3, ['tag']))->__serialize()); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(1) { |
||||||
|
["count"]=> |
||||||
|
NULL |
||||||
|
} |
||||||
|
array(3) { |
||||||
|
["name"]=> |
||||||
|
string(7) "changed" |
||||||
|
["count"]=> |
||||||
|
int(3) |
||||||
|
["extra"]=> |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
string(3) "tag" |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue