parent
ed3f166997
commit
898c1fffdc
25 changed files with 1275 additions and 0 deletions
@ -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…
Reference in new issue