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