test(aot): 添加 Symfony 模式测试用例

pull/15/head
韩天峰 2 months ago
parent ed3f166997
commit 898c1fffdc
  1. 66
      tests/aot/symfony/bluesky-media-associative-destructuring.phpt
  2. 74
      tests/aot/symfony/config-glob-recursive-callback-filter.phpt
  3. 86
      tests/aot/symfony/config-loader-resource-message.phpt
  4. 41
      tests/aot/symfony/config-resource-iterator-cache.phpt
  5. 78
      tests/aot/symfony/console-input-unparse-match-array-map.phpt
  6. 66
      tests/aot/symfony/console-tree-node-from-values.phpt
  7. 36
      tests/aot/symfony/contracts-reflection-type-string-fallback.phpt
  8. 37
      tests/aot/symfony/doctrine-manager-match-assignment-condition.phpt
  9. 57
      tests/aot/symfony/doctrine-mapentity-clone-defaults.phpt
  10. 25
      tests/aot/symfony/errorhandler-coalesce-assign-negated-condition.phpt
  11. 61
      tests/aot/symfony/errorhandler-stringable-context.phpt
  12. 43
      tests/aot/symfony/expressionlanguage-dynamic-call-unpack.phpt
  13. 52
      tests/aot/symfony/htmlsanitizer-clone-unset-nested-attrs.phpt
  14. 70
      tests/aot/symfony/messenger-nullsafe-transport-fallback.phpt
  15. 41
      tests/aot/symfony/notifier-array-key-first-exception.phpt
  16. 70
      tests/aot/symfony/notifier-options-nullsafe-coalesce-assign.phpt
  17. 44
      tests/aot/symfony/objectmapper-weakmap-cache.phpt
  18. 43
      tests/aot/symfony/routing-enum-requirement-cases.phpt
  19. 33
      tests/aot/symfony/routing-extra-query-udiff-replace.phpt
  20. 45
      tests/aot/symfony/routing-null-coalesce-assign-comparison.phpt
  21. 19
      tests/aot/symfony/serializer-cache-key-rawurlencode-strtr.phpt
  22. 53
      tests/aot/symfony/serializer-debug-compact-trace.phpt
  23. 39
      tests/aot/symfony/serializer-reflection-property-initialized.phpt
  24. 52
      tests/aot/symfony/serializer-supported-type-collection.phpt
  25. 44
      tests/aot/symfony/typecontext-nested-cache-coalesce-object.phpt

@ -0,0 +1,66 @@
--TEST--
Symfony Notifier Bluesky pattern: foreach associative array destructuring
--FILE--
<?php
final class MediaFile
{
public function __construct(private string $name, private string $contentType)
{
}
public function getContentType(): string
{
return $this->contentType;
}
public function getName(): string
{
return $this->name;
}
}
function describe_media(array $media): array
{
$uploaded = [];
foreach ($media as ['file' => $file, 'description' => $description]) {
$uploaded[] = [
'alt' => $description,
'name' => $file->getName(),
'mimeType' => $file->getContentType(),
];
}
return $uploaded;
}
function main(): void
{
var_dump(describe_media([
['file' => new MediaFile('first.png', 'image/png'), 'description' => 'First'],
['description' => 'Second', 'file' => new MediaFile('second.jpg', 'image/jpeg')],
]));
}
?>
--EXPECT--
array(2) {
[0]=>
array(3) {
["alt"]=>
string(5) "First"
["name"]=>
string(9) "first.png"
["mimeType"]=>
string(9) "image/png"
}
[1]=>
array(3) {
["alt"]=>
string(6) "Second"
["name"]=>
string(10) "second.jpg"
["mimeType"]=>
string(10) "image/jpeg"
}
}

@ -0,0 +1,74 @@
--TEST--
Symfony Config pattern: RecursiveCallbackFilterIterator and iterator_to_array
--FILE--
<?php
function visible_files(string $root, array $excludedPrefixes): array
{
$prefixLen = strlen($root) + 1;
$files = iterator_to_array(new RecursiveIteratorIterator(
new RecursiveCallbackFilterIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS),
static fn (SplFileInfo $file, string $path): bool => !isset($excludedPrefixes[$path = str_replace('\\', '/', $path)])
&& (str_ends_with($path, '.php') || $file->isDir())
&& '.' !== $file->getBasename()[0]
),
RecursiveIteratorIterator::LEAVES_ONLY
));
uksort($files, 'strnatcmp');
$relative = [];
foreach ($files as $path => $info) {
if ($info->isFile()) {
$relative[] = substr(str_replace('\\', '/', $path), $prefixLen);
}
}
return $relative;
}
function main(): void
{
$root = sys_get_temp_dir().'/aot_symfony_glob_case';
cleanup_glob_fixture($root);
@mkdir($root.'/src/skip', 0777, true);
@mkdir($root.'/src/.hidden', 0777, true);
file_put_contents($root.'/src/App.php', '<?php');
file_put_contents($root.'/src/App.txt', 'txt');
file_put_contents($root.'/src/skip/Ignored.php', '<?php');
file_put_contents($root.'/src/.hidden/Hidden.php', '<?php');
$excluded = [str_replace('\\', '/', $root.'/src/skip') => true];
var_dump(visible_files($root, $excluded));
cleanup_glob_fixture($root);
}
function cleanup_glob_fixture(string $root): void
{
@unlink($root.'/src/.hidden/Hidden.php');
@rmdir($root.'/src/.hidden');
@unlink($root.'/src/skip/Ignored.php');
@rmdir($root.'/src/skip');
@unlink($root.'/src/App.php');
@unlink($root.'/src/App.txt');
@rmdir($root.'/src');
@rmdir($root);
}
?>
--CLEAN--
<?php
$root = sys_get_temp_dir().'/aot_symfony_glob_case';
@unlink($root.'/src/.hidden/Hidden.php');
@rmdir($root.'/src/.hidden');
@unlink($root.'/src/skip/Ignored.php');
@rmdir($root.'/src/skip');
@unlink($root.'/src/App.php');
@unlink($root.'/src/App.txt');
@rmdir($root.'/src');
@rmdir($root);
?>
--EXPECT--
array(1) {
[0]=>
string(11) "src/App.php"
}

@ -0,0 +1,86 @@
--TEST--
Symfony Config pattern: loader exception resource formatting and bundle hints
--FILE--
<?php
function resource_to_string(mixed $var): string
{
if (is_object($var)) {
return sprintf('Object(%s)', $var::class);
}
if (is_array($var)) {
$parts = [];
foreach ($var as $k => $v) {
$parts[] = sprintf('%s => %s', $k, resource_to_string($v));
}
return sprintf('Array(%s)', implode(', ', $parts));
}
if (is_resource($var)) {
return sprintf('Resource(%s)', get_resource_type($var));
}
if (null === $var) {
return 'null';
}
return (string) $var;
}
function build_loader_message(mixed $resource, ?Throwable $previous = null, ?string $sourceResource = null, ?string $type = null): string
{
if (!is_string($resource)) {
try {
$resource = json_encode($resource, JSON_THROW_ON_ERROR);
} catch (JsonException) {
$resource = sprintf('resource of type "%s"', get_debug_type($resource));
}
}
$message = '';
if ($previous) {
if (str_ends_with($previous->getMessage(), '.')) {
$message .= sprintf('%s', substr($previous->getMessage(), 0, -1)).' in ';
} else {
$message .= sprintf('%s', $previous->getMessage()).' in ';
}
$message .= $resource.' ';
$message .= null === $sourceResource
? sprintf('(which is loaded in resource "%s")', $resource)
: sprintf('(which is being imported from "%s")', $sourceResource);
$message .= '.';
} elseif (null === $sourceResource) {
$message .= sprintf('Cannot load resource "%s".', $resource);
} else {
$message .= sprintf('Cannot import resource "%s" from "%s".', $resource, $sourceResource);
}
if ('@' === $resource[0]) {
$parts = explode(DIRECTORY_SEPARATOR, $resource);
$bundle = substr($parts[0], 1);
$message .= sprintf(' Make sure the "%s" bundle is registered.', $bundle);
} elseif (null !== $type) {
$message .= sprintf(' Make sure there is a loader supporting the "%s" type.', $type);
}
return $message;
}
final class ResourceObject
{
}
function main(): void
{
$handle = fopen('php://memory', 'r');
var_dump(resource_to_string(['handle' => $handle, 'object' => new ResourceObject(), 'none' => null]));
var_dump(build_loader_message('@DemoBundle/config.yaml', new RuntimeException('broken.'), null));
var_dump(build_loader_message(['config' => new ResourceObject()], null, 'services.yaml', 'yaml'));
}
?>
--EXPECTF--
string(%d) "Array(handle => Resource(stream), object => Object(ResourceObject), none => null)"
string(%d) "broken in @DemoBundle/config.yaml (which is loaded in resource "@DemoBundle/config.yaml"). Make sure the "DemoBundle" bundle is registered."
string(%d) "Cannot import resource "{"config":{}}" from "services.yaml". Make sure there is a loader supporting the "yaml" type."

@ -0,0 +1,41 @@
--TEST--
Symfony Config pattern: cache Traversable as array with iterator_to_array
--FILE--
<?php
final class ResourceCheckerCache
{
public function __construct(private iterable $resourceCheckers)
{
}
public function all(): array
{
if (!$this->resourceCheckers instanceof Traversable) {
return $this->resourceCheckers;
}
return $this->resourceCheckers = iterator_to_array($this->resourceCheckers);
}
}
function main(): void
{
$cache = new ResourceCheckerCache(new ArrayIterator(['php' => true, 'yaml' => false]));
var_dump($cache->all());
var_dump($cache->all());
}
?>
--EXPECT--
array(2) {
["php"]=>
bool(true)
["yaml"]=>
bool(false)
}
array(2) {
["php"]=>
bool(true)
["yaml"]=>
bool(false)
}

@ -0,0 +1,78 @@
--TEST--
Symfony Console pattern: match(true) returns arrays and array_map option expansion
--FILE--
<?php
final class InputOption
{
public function __construct(
private bool $negatable,
private bool $acceptValue,
private bool $array,
) {
}
public function isNegatable(): bool
{
return $this->negatable;
}
public function acceptValue(): bool
{
return $this->acceptValue;
}
public function isArray(): bool
{
return $this->array;
}
}
function unparse_options(array $rawOptions, array $definition): array
{
$unparsedOptions = [];
foreach ($rawOptions as $optionName => $parsedOption) {
$option = $definition[$optionName];
$unparsedOptions[] = match (true) {
$option->isNegatable() => [sprintf('--%s%s', $parsedOption ? '' : 'no-', $optionName)],
!$option->acceptValue() => [sprintf('--%s', $optionName)],
$option->isArray() => array_map(static fn ($item) => sprintf('--%s=%s', $optionName, $item), $parsedOption),
default => [sprintf('--%s=%s', $optionName, $parsedOption)],
};
}
return array_merge(...$unparsedOptions);
}
function main(): void
{
$definition = [
'ansi' => new InputOption(true, false, false),
'verbose' => new InputOption(false, false, false),
'tag' => new InputOption(false, true, true),
'env' => new InputOption(false, true, false),
];
var_dump(unparse_options([
'ansi' => false,
'verbose' => true,
'tag' => ['api', 'worker'],
'env' => 'prod',
], $definition));
}
?>
--EXPECT--
array(5) {
[0]=>
string(9) "--no-ansi"
[1]=>
string(9) "--verbose"
[2]=>
string(9) "--tag=api"
[3]=>
string(12) "--tag=worker"
[4]=>
string(10) "--env=prod"
}

@ -0,0 +1,66 @@
--TEST--
Symfony Console TreeNode pattern: null coalesce assign and recursive iterable values
--FILE--
<?php
final class TreeNode
{
private array $children = [];
public function __construct(private string $value = 'root')
{
}
public static function fromValues(iterable $nodes, ?self $node = null): self
{
$node ??= new self();
foreach ($nodes as $key => $value) {
if (is_iterable($value)) {
$child = new self((string) $key);
self::fromValues($value, $child);
$node->addChild($child);
} elseif ($value instanceof self) {
$node->addChild($value);
} else {
$node->addChild(new self((string) $value));
}
}
return $node;
}
public function addChild(self $child): void
{
$this->children[] = $child;
}
public function dump(int $level = 0): void
{
echo str_repeat('-', $level), $this->value, "\n";
foreach ($this->children as $child) {
$child->dump($level + 1);
}
}
}
function main(): void
{
TreeNode::fromValues([
'console' => ['input', 'output'],
'http' => ['request' => ['query', 'headers']],
new TreeNode('custom'),
])->dump();
}
?>
--EXPECT--
root
-console
--input
--output
-http
--request
---query
---headers
-custom

@ -0,0 +1,36 @@
--TEST--
Symfony Contracts pattern: ReflectionNamedType getName or ReflectionType string fallback
--FILE--
<?php
final class ServiceDescriptor
{
public function named(): ?string
{
return null;
}
public function union(): int|string|null
{
return 1;
}
}
function describe_return_type(string $method): string
{
$returnType = (new ReflectionMethod(ServiceDescriptor::class, $method))->getReturnType();
$type = $returnType instanceof ReflectionNamedType ? $returnType->getName() : (string) $returnType;
$nullable = $returnType !== null && $returnType->allowsNull();
return ($nullable ? '?' : '').$type;
}
function main(): void
{
var_dump(describe_return_type('named'));
var_dump(describe_return_type('union'));
}
?>
--EXPECT--
string(7) "?string"
string(16) "?string|int|null"

@ -0,0 +1,37 @@
--TEST--
Symfony Doctrine pattern: match true with assignment condition and dynamic method call
--FILE--
<?php
final class ResetContainer
{
private array $methodMap = [
'cache' => 'resetCache',
];
public function reset(string $name): string
{
$method = null;
return match (true) {
!$method = $this->methodMap[$name] ?? null => 'missing',
default => $this->{$method}($name),
};
}
private function resetCache(string $name): string
{
return 'reset:'.$name;
}
}
function main(): void
{
$container = new ResetContainer();
var_dump($container->reset('cache'));
var_dump($container->reset('logger'));
}
?>
--EXPECT--
string(11) "reset:cache"
string(7) "missing"

@ -0,0 +1,57 @@
--TEST--
Symfony Doctrine pattern: clone object and fill defaults with coalesce assign
--FILE--
<?php
class EntityValueResolver
{
}
class DefaultOptions
{
public function __construct(
public ?string $class = null,
public ?string $objectManager = null,
public ?array $mapping = null,
public array|string|null $id = null,
public ?bool $stripNull = null,
) {
}
public function withDefaults(self $defaults, ?string $class): static
{
$clone = clone $this;
$clone->class ??= class_exists($class ?? '') || interface_exists($class ?? '', false) ? $class : null;
$clone->objectManager ??= $defaults->objectManager;
$clone->mapping ??= $defaults->mapping;
$clone->id ??= $defaults->id;
$clone->stripNull ??= $defaults->stripNull ?? false;
return $clone;
}
}
function main(): void
{
$defaults = new DefaultOptions(EntityValueResolver::class, 'default', ['id' => 'uuid'], ['uuid'], true);
$options = new DefaultOptions(null, null, null, 'slug', null);
$merged = $options->withDefaults($defaults, EntityValueResolver::class);
var_dump($merged === $options);
var_dump($merged->class);
var_dump($merged->objectManager);
var_dump($merged->mapping);
var_dump($merged->id);
var_dump($merged->stripNull);
}
?>
--EXPECT--
bool(false)
string(19) "EntityValueResolver"
string(7) "default"
array(1) {
["id"]=>
string(4) "uuid"
}
string(4) "slug"
bool(true)

@ -0,0 +1,25 @@
--TEST--
Symfony ErrorHandler pattern: coalesce assign inside negated condition
--FILE--
<?php
function resolve_handler(?Closure $handler, ?Closure $defaultHandler): string
{
if (!$handler ??= $defaultHandler) {
return 'none';
}
return $handler();
}
function main(): void
{
var_dump(resolve_handler(null, static fn () => 'default'));
var_dump(resolve_handler(static fn () => 'custom', static fn () => 'default'));
var_dump(resolve_handler(null, null));
}
?>
--EXPECT--
string(7) "default"
string(6) "custom"
string(4) "none"

@ -0,0 +1,61 @@
--TEST--
Symfony ErrorHandler pattern: Stringable values in destructured log context
--FILE--
<?php
final class ContextValue implements Stringable
{
public function __construct(private string $value)
{
}
public function __toString(): string
{
return $this->value;
}
}
function normalize_logs(array $logs): array
{
$normalized = [];
foreach ($logs as [$level, $message, $context]) {
foreach ($context as $key => $val) {
if (null === $val || is_scalar($val) || $val instanceof Stringable) {
$context[$key] = (string) $val;
}
}
$normalized[] = [$level, $message, $context];
}
return $normalized;
}
function main(): void
{
var_dump(normalize_logs([
['info', 'boot', ['request' => new ContextValue('GET /'), 'count' => 2, 'skip' => []]],
]));
}
?>
--EXPECT--
array(1) {
[0]=>
array(3) {
[0]=>
string(4) "info"
[1]=>
string(4) "boot"
[2]=>
array(3) {
["request"]=>
string(5) "GET /"
["count"]=>
string(1) "2"
["skip"]=>
array(0) {
}
}
}
}

@ -0,0 +1,43 @@
--TEST--
Symfony ExpressionLanguage pattern: dynamic callable with unpacked evaluated arguments
--FILE--
<?php
final class ExpressionTarget
{
public function join(string $prefix, string ...$parts): string
{
return $prefix.':'.implode(',', $parts);
}
}
final class ArgumentsNode
{
public function __construct(private array $values)
{
}
public function evaluate(array $functions, array $values): array
{
return $this->values + $values + $functions;
}
}
function call_expression_method(object $obj, string $method, ArgumentsNode $arguments): string
{
if (!is_callable($toCall = [$obj, $method])) {
return sprintf('Unable to call method "%s" of object "%s".', $method, get_debug_type($obj));
}
return $toCall(...array_values($arguments->evaluate(['ignored' => 'x'], ['tail' => 'c'])));
}
function main(): void
{
var_dump(call_expression_method(new ExpressionTarget(), 'join', new ArgumentsNode(['a', 'b'])));
var_dump(call_expression_method(new ExpressionTarget(), 'missing', new ArgumentsNode([])));
}
?>
--EXPECT--
string(7) "a:b,c,x"
string(61) "Unable to call method "missing" of object "ExpressionTarget"."

@ -0,0 +1,52 @@
--TEST--
Symfony HtmlSanitizer pattern: clone config, unset nested array state, then write attributes
--FILE--
<?php
final class SanitizerConfig
{
public array $allowedElements = [];
public array $blockedElements = [];
public array $droppedElements = [];
public function allowElement(string $element, array|string $attributes = []): static
{
$clone = clone $this;
unset($clone->blockedElements[$element], $clone->droppedElements[$element]);
$clone->allowedElements[$element] = [];
foreach ((array) $attributes as $allowedAttr) {
$clone->allowedElements[$element][$allowedAttr] = true;
}
return $clone;
}
}
function main(): void
{
$config = new SanitizerConfig();
$config->blockedElements['a'] = true;
$next = $config->allowElement('a', ['href', 'title']);
var_dump($config->blockedElements);
var_dump($next->blockedElements);
var_dump($next->allowedElements);
}
?>
--EXPECT--
array(1) {
["a"]=>
bool(true)
}
array(0) {
}
array(1) {
["a"]=>
array(2) {
["href"]=>
bool(true)
["title"]=>
bool(true)
}
}

@ -0,0 +1,70 @@
--TEST--
Symfony Messenger pattern: nullsafe transport fallback with throw expression
--FILE--
<?php
final class SentToFailureTransportStamp
{
public function __construct(private string $originalReceiverName)
{
}
public function getOriginalReceiverName(): string
{
return $this->originalReceiverName;
}
}
final class ReceivedStamp
{
public function __construct(private string $transportName)
{
}
public function getTransportName(): string
{
return $this->transportName;
}
}
final class Envelope
{
public function __construct(private array $stamps)
{
}
public function last(string $class): ?object
{
foreach (array_reverse($this->stamps) as $stamp) {
if ($stamp instanceof $class) {
return $stamp;
}
}
return null;
}
}
function transport_name(Envelope $envelope): string
{
return $envelope->last(SentToFailureTransportStamp::class)?->getOriginalReceiverName()
?? $envelope->last(ReceivedStamp::class)?->getTransportName()
?? throw new LogicException('A ReceivedStamp is required.');
}
function main(): void
{
var_dump(transport_name(new Envelope([new ReceivedStamp('async')])));
var_dump(transport_name(new Envelope([new ReceivedStamp('async'), new SentToFailureTransportStamp('failed')])));
try {
transport_name(new Envelope([]));
} catch (LogicException $e) {
var_dump($e->getMessage());
}
}
?>
--EXPECT--
string(5) "async"
string(6) "failed"
string(28) "A ReceivedStamp is required."

@ -0,0 +1,41 @@
--TEST--
Symfony Notifier pattern: array_key_first selects wrapped exception
--FILE--
<?php
final class HandlerFailedException extends RuntimeException
{
public function __construct(private array $exceptions)
{
parent::__construct('handler failed');
}
public function getWrappedExceptions(): array
{
return $this->exceptions;
}
}
function unwrap_throwable(Throwable $throwable): Throwable
{
if ($throwable instanceof HandlerFailedException) {
$exceptions = $throwable->getWrappedExceptions();
$throwable = $exceptions[array_key_first($exceptions)];
}
return $throwable;
}
function main(): void
{
$throwable = new HandlerFailedException([
'mail' => new InvalidArgumentException('bad address'),
'sms' => new LogicException('not sent'),
]);
$unwrapped = unwrap_throwable($throwable);
echo get_class($unwrapped), ': ', $unwrapped->getMessage(), "\n";
}
?>
--EXPECT--
InvalidArgumentException: bad address

@ -0,0 +1,70 @@
--TEST--
Symfony Notifier pattern: nullsafe options array with coalesce assign
--FILE--
<?php
final class MessageOptions
{
public function __construct(private array $options)
{
}
public function toArray(): array
{
return $this->options;
}
}
final class ChatMessage
{
public function __construct(
private string $subject,
private ?MessageOptions $options = null,
private ?string $recipientId = null,
) {
}
public function getOptions(): ?MessageOptions
{
return $this->options;
}
public function getRecipientId(): ?string
{
return $this->recipientId;
}
public function getSubject(): string
{
return $this->subject;
}
}
function normalize_chat_payload(ChatMessage $message, string $defaultChannel): array
{
$options = $message->getOptions()?->toArray() ?? [];
$options['channel'] ??= $message->getRecipientId() ?: $defaultChannel;
$options['text'] = $message->getSubject();
return array_filter($options);
}
function main(): void
{
var_dump(normalize_chat_payload(new ChatMessage('deploy', null, null), '#ops'));
var_dump(normalize_chat_payload(new ChatMessage('alert', new MessageOptions(['channel' => '#custom', 'emoji' => ''])), '#ops'));
}
?>
--EXPECT--
array(2) {
["channel"]=>
string(4) "#ops"
["text"]=>
string(6) "deploy"
}
array(2) {
["channel"]=>
string(7) "#custom"
["text"]=>
string(5) "alert"
}

@ -0,0 +1,44 @@
--TEST--
Symfony ObjectMapper pattern: WeakMap object cache with array syntax
--FILE--
<?php
final class Source
{
public function __construct(public string $name)
{
}
}
final class Target
{
public function __construct(public string $name)
{
}
}
function map_source(Source $source, WeakMap $objectMap): Target
{
if (isset($objectMap[$source])) {
return $objectMap[$source];
}
return $objectMap[$source] = new Target(strtoupper($source->name));
}
function main(): void
{
$source = new Source('symfony');
$objectMap = new WeakMap();
$first = map_source($source, $objectMap);
$second = map_source($source, $objectMap);
var_dump($first === $second);
var_dump($first->name);
var_dump(count($objectMap));
}
?>
--EXPECT--
bool(true)
string(7) "SYMFONY"
int(1)

@ -0,0 +1,43 @@
--TEST--
Symfony Routing pattern: enum cases, class coalesce assign, dynamic instanceof
--FILE--
<?php
enum Locale: string
{
case EN = 'en';
case FR = 'fr';
}
enum Status: string
{
case Draft = 'draft';
}
function enum_requirement(array $cases): string
{
$class = null;
foreach ($cases as $case) {
if (!$case instanceof BackedEnum) {
return 'invalid type';
}
$class ??= $case::class;
if (!$case instanceof $class) {
return sprintf('%s::%s not in %s', get_debug_type($case), $case->name, $class);
}
}
return implode('|', array_map(static fn ($e) => preg_quote($e->value), $cases));
}
function main(): void
{
var_dump(enum_requirement([Locale::EN, Locale::FR]));
var_dump(enum_requirement([Locale::EN, Status::Draft]));
}
?>
--EXPECT--
string(5) "en|fr"
string(27) "Status::Draft not in Locale"

@ -0,0 +1,33 @@
--TEST--
Symfony Routing pattern: array_udiff_assoc with array_diff_key and array_replace
--FILE--
<?php
function extra_query(array $parameters, array $variables, array $defaults, array $queryParameters): array
{
$extra = array_udiff_assoc(
array_diff_key($parameters, $variables),
$defaults,
static fn ($a, $b): int => $a == $b ? 0 : 1
);
return array_replace($extra, $queryParameters);
}
function main(): void
{
$parameters = ['slug' => 'post', 'page' => '1', 'sort' => 'new', 'debug' => false];
$variables = ['slug' => true];
$defaults = ['page' => 1, 'sort' => 'old', 'debug' => false];
$query = ['sort' => 'top', 'filter' => 'all'];
var_dump(extra_query($parameters, $variables, $defaults, $query));
}
?>
--EXPECT--
array(2) {
["sort"]=>
string(3) "top"
["filter"]=>
string(3) "all"
}

@ -0,0 +1,45 @@
--TEST--
Symfony Routing pattern: null comparison with coalesce assign route lookup
--FILE--
<?php
final class Route
{
public function __construct(public string $name)
{
}
}
final class RouteCollectionLookup
{
public function __construct(private array $routes)
{
}
public function get(string $name): ?Route
{
return $this->routes[$name] ?? null;
}
}
function resolve_route(RouteCollectionLookup $routes, string $name, ?Route $route = null): string
{
if (null === $route ??= $routes->get($name)) {
return 'missing';
}
return $route->name;
}
function main(): void
{
$routes = new RouteCollectionLookup(['home' => new Route('home')]);
var_dump(resolve_route($routes, 'home'));
var_dump(resolve_route($routes, 'missing'));
var_dump(resolve_route($routes, 'missing', new Route('explicit')));
}
?>
--EXPECT--
string(4) "home"
string(7) "missing"
string(8) "explicit"

@ -0,0 +1,19 @@
--TEST--
Symfony Serializer pattern: cache key with rawurlencode and strtr
--FILE--
<?php
function metadata_cache_key(string $class): string
{
return rawurlencode(strtr($class, '\\', '_'));
}
function main(): void
{
var_dump(metadata_cache_key('Symfony\\Component\\Serializer\\Mapping\\ClassMetadata'));
var_dump(metadata_cache_key('App\\Model\\User Profile'));
}
?>
--EXPECT--
string(50) "Symfony_Component_Serializer_Mapping_ClassMetadata"
string(24) "App_Model_User%20Profile"

@ -0,0 +1,53 @@
--TEST--
Symfony Serializer pattern: isset multiple trace keys and compact return
--FILE--
<?php
function normalize_trace_frame(array $trace, int $i): array
{
$name = 'unknown';
$file = null;
$line = null;
if (isset($trace[$i]['class'], $trace[$i]['function'])) {
$name = $trace[$i]['class'].'::'.$trace[$i]['function'];
} elseif (isset($trace[$i]['function'])) {
$name = $trace[$i]['function'];
}
if (isset($trace[$i]['file'], $trace[$i]['line'])) {
$file = $trace[$i]['file'];
$line = $trace[$i]['line'];
}
return compact('name', 'file', 'line');
}
function main(): void
{
$trace = [
['class' => 'Serializer', 'function' => 'normalize', 'file' => 'TraceableSerializer.php', 'line' => 170],
['function' => 'main'],
];
var_dump(normalize_trace_frame($trace, 0));
var_dump(normalize_trace_frame($trace, 1));
}
?>
--EXPECT--
array(3) {
["name"]=>
string(21) "Serializer::normalize"
["file"]=>
string(23) "TraceableSerializer.php"
["line"]=>
int(170)
}
array(3) {
["name"]=>
string(4) "main"
["file"]=>
NULL
["line"]=>
NULL
}

@ -0,0 +1,39 @@
--TEST--
Symfony Serializer pattern: ReflectionProperty checks typed property initialization state
--FILE--
<?php
final class PartialPayload
{
public ?string $foo;
public ?string $bar;
public ?string $nothing = null;
}
function initialized_properties(object $object, array $names): array
{
$initialized = [];
foreach ($names as $name) {
$initialized[$name] = (new ReflectionProperty($object, $name))->isInitialized($object);
}
return $initialized;
}
function main(): void
{
$payload = new PartialPayload();
$payload->foo = null;
var_dump(initialized_properties($payload, ['foo', 'bar', 'nothing']));
}
?>
--EXPECT--
array(3) {
["foo"]=>
bool(true)
["bar"]=>
bool(true)
["nothing"]=>
bool(true)
}

@ -0,0 +1,52 @@
--TEST--
Symfony Serializer pattern: supported type matching for ClassName array collections
--FILE--
<?php
class Animal
{
}
class Dog extends Animal
{
}
class Cat extends Animal
{
}
function supports_type(string $class, array $supportedTypes): array
{
$genericType = class_exists($class) || interface_exists($class, false) ? 'object' : '*';
$doesClassRepresentCollection = str_ends_with($class, '[]');
$matches = [];
foreach ($supportedTypes as $supportedType => $isCacheable) {
if (in_array($supportedType, ['*', 'object'], true)
|| $class !== $supportedType && ('object' !== $genericType || !is_subclass_of($class, $supportedType))
&& !($doesClassRepresentCollection && str_ends_with($supportedType, '[]') && is_subclass_of(strstr($class, '[]', true), strstr($supportedType, '[]', true)))
) {
continue;
}
$matches[$supportedType] = $isCacheable;
}
return $matches;
}
function main(): void
{
var_dump(supports_type(Dog::class, [Animal::class => true, Cat::class => false, '*' => null]));
var_dump(supports_type(Dog::class.'[]', [Animal::class.'[]' => true, Cat::class.'[]' => false, 'object' => null]));
}
?>
--EXPECT--
array(1) {
["Animal"]=>
bool(true)
}
array(1) {
["Animal[]"]=>
bool(true)
}

@ -0,0 +1,44 @@
--TEST--
Symfony TypeInfo pattern: nested array cache with coalesce assign returns object
--FILE--
<?php
final class TypeContext
{
public function __construct(
public string $calledClass,
public string $declaringClass,
) {
}
}
final class TypeContextFactory
{
private array $typeContextCache = [];
public function createFromClassName(string $calledClassName, ?string $declaringClassName = null): TypeContext
{
$declaringClassName ??= $calledClassName;
return $this->typeContextCache[$declaringClassName][$calledClassName] ??= new TypeContext($calledClassName, $declaringClassName);
}
}
function main(): void
{
$factory = new TypeContextFactory();
$first = $factory->createFromClassName('Child', 'Parent');
$second = $factory->createFromClassName('Child', 'Parent');
$third = $factory->createFromClassName('Other');
var_dump($first === $second);
var_dump($first->calledClass, $first->declaringClass);
var_dump($third->calledClass, $third->declaringClass);
}
?>
--EXPECT--
bool(true)
string(5) "Child"
string(6) "Parent"
string(5) "Other"
string(5) "Other"
Loading…
Cancel
Save