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