parent
035b1b09d7
commit
f0e025dad4
21 changed files with 1038 additions and 40 deletions
@ -0,0 +1,69 @@ |
||||
--TEST-- |
||||
Symfony Serializer pattern: match true used for metadata side effects |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class GroupsAttribute |
||||
{ |
||||
public function __construct(public array $groups) |
||||
{ |
||||
} |
||||
} |
||||
|
||||
class IgnoreAttribute |
||||
{ |
||||
} |
||||
|
||||
class MaxDepthAttribute |
||||
{ |
||||
public function __construct(public int $maxDepth) |
||||
{ |
||||
} |
||||
} |
||||
|
||||
class Metadata |
||||
{ |
||||
public array $groups = []; |
||||
public bool $ignored = false; |
||||
public ?int $maxDepth = null; |
||||
|
||||
public function addGroup(string $group): void |
||||
{ |
||||
$this->groups[] = $group; |
||||
} |
||||
} |
||||
|
||||
function applyAttribute(object $attribute, Metadata $metadata): void |
||||
{ |
||||
match (true) { |
||||
$attribute instanceof MaxDepthAttribute => $metadata->maxDepth = $attribute->maxDepth, |
||||
$attribute instanceof IgnoreAttribute => $metadata->ignored = true, |
||||
$attribute instanceof GroupsAttribute => array_map($metadata->addGroup(...), $attribute->groups), |
||||
default => null, |
||||
}; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$metadata = new Metadata(); |
||||
applyAttribute(new GroupsAttribute(['read', 'write']), $metadata); |
||||
applyAttribute(new MaxDepthAttribute(3), $metadata); |
||||
applyAttribute(new IgnoreAttribute(), $metadata); |
||||
|
||||
var_dump($metadata); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
object(Metadata)#1 (3) { |
||||
["groups"]=> |
||||
array(2) { |
||||
[0]=> |
||||
string(4) "read" |
||||
[1]=> |
||||
string(5) "write" |
||||
} |
||||
["ignored"]=> |
||||
bool(true) |
||||
["maxDepth"]=> |
||||
int(3) |
||||
} |
||||
@ -0,0 +1,68 @@ |
||||
--TEST-- |
||||
Symfony DI pattern: filtered attribute nullsafe property with coalesce assignment |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class TargetAttribute |
||||
{ |
||||
public function __construct(public ?string $name) |
||||
{ |
||||
} |
||||
} |
||||
|
||||
class ServiceReference |
||||
{ |
||||
public function __construct( |
||||
private string $name, |
||||
private array $attributes, |
||||
) { |
||||
} |
||||
|
||||
public function getAttributes(): array |
||||
{ |
||||
return $this->attributes; |
||||
} |
||||
|
||||
public function getName(): ?string |
||||
{ |
||||
return $this->name; |
||||
} |
||||
} |
||||
|
||||
function resolveTarget(ServiceReference $reference): array |
||||
{ |
||||
$name = $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof TargetAttribute)[0] ?? null)?->name; |
||||
|
||||
if (null !== $name ??= $reference->getName()) { |
||||
return [$name, $target]; |
||||
} |
||||
|
||||
return ['missing', $target]; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(resolveTarget(new ServiceReference('fallback', [new TargetAttribute('explicit')]))); |
||||
var_dump(resolveTarget(new ServiceReference('fallback', []))); |
||||
var_dump(resolveTarget(new ServiceReference('fallback', [new stdClass(), new TargetAttribute(null)]))); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(2) { |
||||
[0]=> |
||||
string(8) "explicit" |
||||
[1]=> |
||||
string(8) "explicit" |
||||
} |
||||
array(2) { |
||||
[0]=> |
||||
string(8) "fallback" |
||||
[1]=> |
||||
NULL |
||||
} |
||||
array(2) { |
||||
[0]=> |
||||
string(8) "fallback" |
||||
[1]=> |
||||
NULL |
||||
} |
||||
@ -0,0 +1,78 @@ |
||||
--TEST-- |
||||
Symfony Console pattern: nullsafe attribute values with coalesce assignment |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class CommandAttribute |
||||
{ |
||||
public function __construct( |
||||
public ?string $description = null, |
||||
public ?string $help = null, |
||||
public ?array $usages = null, |
||||
) { |
||||
} |
||||
} |
||||
|
||||
function normalizeCommandMetadata(?CommandAttribute $attribute, array $tag): array |
||||
{ |
||||
$description = null; |
||||
$help = null; |
||||
$usages = null; |
||||
|
||||
$description ??= $tag['description'] ?? null; |
||||
$help ??= $tag['help'] ?? null; |
||||
$usages ??= $tag['usages'] ?? null; |
||||
|
||||
if ($help ??= $attribute?->help) { |
||||
$help = trim($help); |
||||
} |
||||
if ($usages ??= $attribute?->usages) { |
||||
$usages = array_values($usages); |
||||
} |
||||
if ($description ??= $attribute?->description) { |
||||
$description = strtoupper($description); |
||||
} |
||||
|
||||
return [$description, $help, $usages]; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(normalizeCommandMetadata(new CommandAttribute('from attribute', ' help ', ['a', 'b']), [])); |
||||
var_dump(normalizeCommandMetadata(new CommandAttribute('ignored', 'ignored', ['x']), ['description' => 'from tag', 'help' => 'tag help'])); |
||||
var_dump(normalizeCommandMetadata(null, [])); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(3) { |
||||
[0]=> |
||||
string(14) "FROM ATTRIBUTE" |
||||
[1]=> |
||||
string(4) "help" |
||||
[2]=> |
||||
array(2) { |
||||
[0]=> |
||||
string(1) "a" |
||||
[1]=> |
||||
string(1) "b" |
||||
} |
||||
} |
||||
array(3) { |
||||
[0]=> |
||||
string(8) "FROM TAG" |
||||
[1]=> |
||||
string(8) "tag help" |
||||
[2]=> |
||||
array(1) { |
||||
[0]=> |
||||
string(1) "x" |
||||
} |
||||
} |
||||
array(3) { |
||||
[0]=> |
||||
NULL |
||||
[1]=> |
||||
NULL |
||||
[2]=> |
||||
NULL |
||||
} |
||||
@ -0,0 +1,35 @@ |
||||
--TEST-- |
||||
Symfony Console pattern: closure caches iterable values with coalesce assignment |
||||
--FILE-- |
||||
<?php |
||||
|
||||
function valueCallback(iterable $values): Closure |
||||
{ |
||||
$valueCache = null; |
||||
|
||||
return static function () use (&$valueCache, $values): array { |
||||
return $valueCache ??= iterator_to_array($values, false); |
||||
}; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$callback = valueCallback(new ArrayIterator(['a' => 'A', 'b' => 'B'])); |
||||
|
||||
var_dump($callback()); |
||||
var_dump($callback()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(2) { |
||||
[0]=> |
||||
string(1) "A" |
||||
[1]=> |
||||
string(1) "B" |
||||
} |
||||
array(2) { |
||||
[0]=> |
||||
string(1) "A" |
||||
[1]=> |
||||
string(1) "B" |
||||
} |
||||
@ -0,0 +1,31 @@ |
||||
--TEST-- |
||||
Symfony DI pattern: normalize named arguments with array_combine and preg_replace |
||||
--FILE-- |
||||
<?php |
||||
|
||||
function normalizeArguments(array $arguments): array |
||||
{ |
||||
return array_combine( |
||||
array_map(static fn ($key) => preg_replace('/^.*\$/', '', $key), array_keys($arguments)), |
||||
$arguments, |
||||
); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(normalizeArguments([ |
||||
'App\Service $mailer' => 'smtp', |
||||
'$logger' => 'file', |
||||
'plain' => 'value', |
||||
])); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(3) { |
||||
["mailer"]=> |
||||
string(4) "smtp" |
||||
["logger"]=> |
||||
string(4) "file" |
||||
["plain"]=> |
||||
string(5) "value" |
||||
} |
||||
@ -0,0 +1,38 @@ |
||||
--TEST-- |
||||
Symfony HttpKernel pattern: controller reflector match with unpacked callable |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class DemoController |
||||
{ |
||||
public function action(): void |
||||
{ |
||||
} |
||||
|
||||
public static function staticAction(): void |
||||
{ |
||||
} |
||||
} |
||||
|
||||
function reflectorOf(callable|string $controller): ReflectionFunctionAbstract |
||||
{ |
||||
return match (true) { |
||||
is_array($controller) && method_exists(...$controller) => new ReflectionMethod(...$controller), |
||||
is_string($controller) && str_contains($controller, '::') => new ReflectionMethod(...explode('::', $controller, 2)), |
||||
default => new ReflectionFunction($controller(...)), |
||||
}; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$object = new DemoController(); |
||||
|
||||
var_dump(reflectorOf([$object, 'action'])->getName()); |
||||
var_dump(reflectorOf(DemoController::class.'::staticAction')->getName()); |
||||
var_dump(reflectorOf(static fn () => null)->isClosure()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(6) "action" |
||||
string(12) "staticAction" |
||||
bool(true) |
||||
@ -0,0 +1,35 @@ |
||||
--TEST-- |
||||
Symfony HttpFoundation pattern: accept header specificity with explode defaults and match |
||||
--FILE-- |
||||
<?php |
||||
|
||||
function specificity(string $rangeValue, int $paramCount, bool $isQueryMedia, bool $isRangeMedia): int |
||||
{ |
||||
if (!$isQueryMedia && !$isRangeMedia) { |
||||
return ('*' !== $rangeValue ? 2000 : 1000) + $paramCount; |
||||
} |
||||
|
||||
[$rangeType, $rangeSubtype] = explode('/', $rangeValue, 2) + [1 => '*']; |
||||
|
||||
$specificity = match (true) { |
||||
'*' !== $rangeSubtype => 3000, |
||||
'*' !== $rangeType => 2000, |
||||
default => 1000, |
||||
}; |
||||
|
||||
return $specificity + $paramCount; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(specificity('text/plain', 2, true, true)); |
||||
var_dump(specificity('text/*', 1, true, true)); |
||||
var_dump(specificity('*', 0, true, true)); |
||||
var_dump(specificity('application/json', 3, false, false)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(3002) |
||||
int(2001) |
||||
int(1000) |
||||
int(2003) |
||||
@ -0,0 +1,49 @@ |
||||
--TEST-- |
||||
Symfony HttpFoundation pattern: null default new ArrayObject and new static JSON factory |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class JsonResponseLike |
||||
{ |
||||
public mixed $data; |
||||
public bool $json; |
||||
|
||||
public function __construct(mixed $data = null, bool $json = false) |
||||
{ |
||||
if ($json && !is_string($data) && !is_numeric($data) && !$data instanceof Stringable) { |
||||
throw new TypeError(sprintf('bad data "%s"', get_debug_type($data))); |
||||
} |
||||
|
||||
$data ??= new ArrayObject(); |
||||
$this->data = $data; |
||||
$this->json = $json; |
||||
} |
||||
|
||||
public static function fromJsonString(string $data): static |
||||
{ |
||||
return new static($data, true); |
||||
} |
||||
} |
||||
|
||||
class CustomJsonResponseLike extends JsonResponseLike |
||||
{ |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$response = new JsonResponseLike(); |
||||
var_dump($response->data instanceof ArrayObject); |
||||
var_dump($response->json); |
||||
|
||||
$custom = CustomJsonResponseLike::fromJsonString('{"ok":true}'); |
||||
var_dump($custom::class); |
||||
var_dump($custom->data); |
||||
var_dump($custom->json); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
bool(true) |
||||
bool(false) |
||||
string(22) "CustomJsonResponseLike" |
||||
string(11) "{"ok":true}" |
||||
bool(true) |
||||
@ -0,0 +1,70 @@ |
||||
--TEST-- |
||||
Symfony Mailer style self::method(...) callback in array_map |
||||
--FILE-- |
||||
<?php |
||||
class Address |
||||
{ |
||||
public function __construct(private string $address, private string $name = '') {} |
||||
|
||||
public function getAddress(): string |
||||
{ |
||||
return $this->address; |
||||
} |
||||
|
||||
public function getName(): string |
||||
{ |
||||
return $this->name; |
||||
} |
||||
} |
||||
|
||||
class PayloadBuilder |
||||
{ |
||||
public static function build(array $to, array $cc, array $bcc): array |
||||
{ |
||||
return [ |
||||
'to' => array_map(self::encodeEmail(...), $to), |
||||
'cc' => array_map(self::encodeEmail(...), $cc), |
||||
'bcc' => array_map(self::encodeEmail(...), $bcc), |
||||
]; |
||||
} |
||||
|
||||
private static function encodeEmail(Address $address): array |
||||
{ |
||||
return array_filter(['email' => $address->getAddress(), 'name' => $address->getName()]); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$payload = PayloadBuilder::build( |
||||
[new Address('a@example.com', 'Alice')], |
||||
[new Address('b@example.com')], |
||||
[] |
||||
); |
||||
var_dump($payload); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(3) { |
||||
["to"]=> |
||||
array(1) { |
||||
[0]=> |
||||
array(2) { |
||||
["email"]=> |
||||
string(13) "a@example.com" |
||||
["name"]=> |
||||
string(5) "Alice" |
||||
} |
||||
} |
||||
["cc"]=> |
||||
array(1) { |
||||
[0]=> |
||||
array(1) { |
||||
["email"]=> |
||||
string(13) "b@example.com" |
||||
} |
||||
} |
||||
["bcc"]=> |
||||
array(0) { |
||||
} |
||||
} |
||||
@ -0,0 +1,42 @@ |
||||
--TEST-- |
||||
Symfony HttpKernel pattern: match true with assignment inside condition |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class Metadata |
||||
{ |
||||
public function __construct(private string $value) |
||||
{ |
||||
} |
||||
|
||||
public function evaluate(): string |
||||
{ |
||||
return 'metadata:'.$this->value; |
||||
} |
||||
} |
||||
|
||||
class EventWithMetadata |
||||
{ |
||||
public ?Metadata $controllerMetadata = null; |
||||
} |
||||
|
||||
function evaluate(EventWithMetadata $event): string |
||||
{ |
||||
return match (true) { |
||||
($m = $event->controllerMetadata ?? null) instanceof Metadata => $m->evaluate(), |
||||
default => 'none', |
||||
}; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$event = new EventWithMetadata(); |
||||
var_dump(evaluate($event)); |
||||
|
||||
$event->controllerMetadata = new Metadata('ok'); |
||||
var_dump(evaluate($event)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(4) "none" |
||||
string(11) "metadata:ok" |
||||
@ -0,0 +1,33 @@ |
||||
--TEST-- |
||||
Symfony Config pattern: match true formats scalar values or throws by debug type |
||||
--FILE-- |
||||
<?php |
||||
|
||||
function displayValue(mixed $value): string|int |
||||
{ |
||||
return match (true) { |
||||
is_int($value) => $value, |
||||
is_string($value) => sprintf('"%s"', $value), |
||||
is_bool($value) => throw new InvalidArgumentException(sprintf('unsupported "%s"', get_debug_type($value))), |
||||
default => sprintf('of type "%s"', get_debug_type($value)), |
||||
}; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(displayValue(7)); |
||||
var_dump(displayValue('name')); |
||||
var_dump(displayValue([])); |
||||
|
||||
try { |
||||
displayValue(false); |
||||
} catch (InvalidArgumentException $e) { |
||||
echo $e->getMessage(), "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(7) |
||||
string(6) ""name"" |
||||
string(15) "of type "array"" |
||||
unsupported "bool" |
||||
@ -0,0 +1,91 @@ |
||||
--TEST-- |
||||
Symfony style Envelope clone with variadic stamps and dynamic stamp removal |
||||
--FILE-- |
||||
<?php |
||||
interface StampInterface {} |
||||
interface RemovableStampInterface extends StampInterface {} |
||||
|
||||
class FirstStamp implements RemovableStampInterface |
||||
{ |
||||
public function __construct(public string $name) {} |
||||
} |
||||
|
||||
class SecondStamp implements StampInterface |
||||
{ |
||||
public function __construct(public string $name) {} |
||||
} |
||||
|
||||
final class Envelope |
||||
{ |
||||
private array $stamps = []; |
||||
|
||||
public function __construct(private object $message, array $stamps = []) |
||||
{ |
||||
foreach ($stamps as $stamp) { |
||||
$this->stamps[$stamp::class][] = $stamp; |
||||
} |
||||
} |
||||
|
||||
public static function wrap(object $message, array $stamps = []): self |
||||
{ |
||||
$envelope = $message instanceof self ? $message : new self($message); |
||||
|
||||
return $envelope->with(...$stamps); |
||||
} |
||||
|
||||
public function with(StampInterface ...$stamps): static |
||||
{ |
||||
$cloned = clone $this; |
||||
|
||||
foreach ($stamps as $stamp) { |
||||
$cloned->stamps[$stamp::class][] = $stamp; |
||||
} |
||||
|
||||
return $cloned; |
||||
} |
||||
|
||||
public function withoutStampsOfType(string $type): self |
||||
{ |
||||
$cloned = clone $this; |
||||
|
||||
foreach ($cloned->stamps as $class => $stamps) { |
||||
if ($class === $type || is_subclass_of($class, $type)) { |
||||
unset($cloned->stamps[$class]); |
||||
} |
||||
} |
||||
|
||||
return $cloned; |
||||
} |
||||
|
||||
public function all(?string $stampFqcn = null): array |
||||
{ |
||||
if (null !== $stampFqcn) { |
||||
return $this->stamps[$stampFqcn] ?? []; |
||||
} |
||||
|
||||
return $this->stamps; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$envelope = Envelope::wrap(new stdClass(), [new FirstStamp('a'), new SecondStamp('b')]); |
||||
$filtered = $envelope->withoutStampsOfType(RemovableStampInterface::class); |
||||
var_dump(array_keys($envelope->all())); |
||||
var_dump(array_keys($filtered->all())); |
||||
var_dump($filtered->all(FirstStamp::class)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(2) { |
||||
[0]=> |
||||
string(10) "FirstStamp" |
||||
[1]=> |
||||
string(11) "SecondStamp" |
||||
} |
||||
array(1) { |
||||
[0]=> |
||||
string(11) "SecondStamp" |
||||
} |
||||
array(0) { |
||||
} |
||||
@ -0,0 +1,44 @@ |
||||
--TEST-- |
||||
Symfony Messenger style nullsafe chain with coalesce throw |
||||
--FILE-- |
||||
<?php |
||||
class MissingStampException extends Exception {} |
||||
|
||||
class TransportStamp |
||||
{ |
||||
public function __construct(private string $id) {} |
||||
|
||||
public function getId(): string |
||||
{ |
||||
return $this->id; |
||||
} |
||||
} |
||||
|
||||
class Envelope |
||||
{ |
||||
public function __construct(private ?TransportStamp $stamp) {} |
||||
|
||||
public function last(string $class): ?object |
||||
{ |
||||
return $this->stamp instanceof $class ? $this->stamp : null; |
||||
} |
||||
} |
||||
|
||||
function stampId(Envelope $envelope): string |
||||
{ |
||||
return $envelope->last(TransportStamp::class)?->getId() ?? throw new MissingStampException('No stamp found.'); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(stampId(new Envelope(new TransportStamp('abc')))); |
||||
try { |
||||
stampId(new Envelope(null)); |
||||
} catch (MissingStampException $e) { |
||||
var_dump($e->getMessage()); |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(3) "abc" |
||||
string(15) "No stamp found." |
||||
@ -0,0 +1,19 @@ |
||||
--TEST-- |
||||
Symfony DI pattern: indent generated code lines with explode array_map implode |
||||
--FILE-- |
||||
<?php |
||||
|
||||
function indentCode(string $code): string |
||||
{ |
||||
return implode("\n", array_map(static fn ($line) => $line ? ' '.$line : $line, explode("\n", $code))); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(indentCode("first\n\nsecond")); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(21) " first |
||||
|
||||
second" |
||||
@ -0,0 +1,48 @@ |
||||
--TEST-- |
||||
Symfony style RememberMeDetails new static(...array after unset) |
||||
--FILE-- |
||||
<?php |
||||
class AuthenticationException extends Exception {} |
||||
|
||||
class RememberMeDetails |
||||
{ |
||||
public const COOKIE_DELIMITER = ':'; |
||||
|
||||
public function __construct( |
||||
public string $userIdentifier, |
||||
public int $expires, |
||||
public string $value, |
||||
) { |
||||
var_dump(get_debug_type($userIdentifier), get_debug_type($expires), get_debug_type($value)); |
||||
} |
||||
|
||||
public static function fromRawCookie(string $rawCookie): self |
||||
{ |
||||
if (!str_contains($rawCookie, self::COOKIE_DELIMITER)) { |
||||
$rawCookie = 'prefix'.self::COOKIE_DELIMITER.$rawCookie; |
||||
} |
||||
|
||||
$cookieParts = explode(self::COOKIE_DELIMITER, $rawCookie, 4); |
||||
|
||||
if (4 !== count($cookieParts)) { |
||||
throw new AuthenticationException('The cookie contains invalid data.'); |
||||
} |
||||
|
||||
unset($cookieParts[0]); |
||||
|
||||
return new static(...$cookieParts); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$raw = 'prefix:user-name:12345:series-token'; |
||||
$details = RememberMeDetails::fromRawCookie($raw); |
||||
var_dump($details::class); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(6) "string" |
||||
string(3) "int" |
||||
string(6) "string" |
||||
string(17) "RememberMeDetails" |
||||
@ -0,0 +1,36 @@ |
||||
--TEST-- |
||||
Symfony Routing pattern: provider returns callable invoked with unpacked args |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class FunctionProvider |
||||
{ |
||||
/** @var array<string, callable> */ |
||||
private array $functions = []; |
||||
|
||||
public function set(string $name, callable $function): void |
||||
{ |
||||
$this->functions[$name] = $function; |
||||
} |
||||
|
||||
public function get(string $name): callable |
||||
{ |
||||
return $this->functions[$name]; |
||||
} |
||||
} |
||||
|
||||
function evaluate(FunctionProvider $provider, string $function, array $args): string |
||||
{ |
||||
return $provider->get($function)(...$args); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$provider = new FunctionProvider(); |
||||
$provider->set('format', static fn (string $prefix, string $value): string => $prefix.':'.strtoupper($value)); |
||||
|
||||
var_dump(evaluate($provider, 'format', ['name', 'symfony'])); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(12) "name:SYMFONY" |
||||
@ -0,0 +1,59 @@ |
||||
--TEST-- |
||||
Symfony Routing pattern: extra parameters diff and recursive object caster |
||||
--SKIPIF-- |
||||
<?php exit('skip closures do not support reference parameters'); ?> |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class Slug |
||||
{ |
||||
public function __construct(private string $value) |
||||
{ |
||||
} |
||||
|
||||
public function __toString(): string |
||||
{ |
||||
return $this->value; |
||||
} |
||||
} |
||||
|
||||
function normalizeExtra(array $parameters, array $variables, array $defaults, array $queryParameters): array |
||||
{ |
||||
$extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, static fn ($a, $b) => $a == $b ? 0 : 1); |
||||
$extra = array_replace($extra, $queryParameters); |
||||
|
||||
array_walk_recursive($extra, $caster = static function (&$value) use (&$caster): void { |
||||
if (is_object($value)) { |
||||
$value = (string) $value; |
||||
} elseif (is_array($value)) { |
||||
array_walk_recursive($value, $caster); |
||||
} |
||||
}); |
||||
|
||||
return $extra; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(normalizeExtra( |
||||
['id' => 10, 'page' => 1, 'slug' => new Slug('hello'), 'tags' => [new Slug('a'), new Slug('b')]], |
||||
['id' => true], |
||||
['page' => 1, 'slug' => 'old'], |
||||
['q' => new Slug('search')] |
||||
)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(3) { |
||||
["slug"]=> |
||||
string(5) "hello" |
||||
["tags"]=> |
||||
array(2) { |
||||
[0]=> |
||||
string(1) "a" |
||||
[1]=> |
||||
string(1) "b" |
||||
} |
||||
["q"]=> |
||||
string(6) "search" |
||||
} |
||||
@ -0,0 +1,42 @@ |
||||
--TEST-- |
||||
Symfony Routing pattern: null comparison with route coalesce assignment |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class RouteStore |
||||
{ |
||||
public function __construct(private array $routes) |
||||
{ |
||||
} |
||||
|
||||
public function get(string $name): ?string |
||||
{ |
||||
echo "lookup:$name\n"; |
||||
return $this->routes[$name] ?? null; |
||||
} |
||||
} |
||||
|
||||
function generate(RouteStore $routes, string $name): string |
||||
{ |
||||
$route = null; |
||||
|
||||
if (null === $route ??= $routes->get($name)) { |
||||
return 'missing'; |
||||
} |
||||
|
||||
return 'route:'.$route; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$routes = new RouteStore(['home' => '/']); |
||||
|
||||
var_dump(generate($routes, 'home')); |
||||
var_dump(generate($routes, 'missing')); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
lookup:home |
||||
string(7) "route:/" |
||||
lookup:missing |
||||
string(7) "missing" |
||||
@ -0,0 +1,44 @@ |
||||
--TEST-- |
||||
Symfony style dynamic instanceof against pipe-separated type string |
||||
--FILE-- |
||||
<?php |
||||
interface UserInterface {} |
||||
class AdminUser implements UserInterface {} |
||||
class GuestUser {} |
||||
|
||||
function resolveUser(object $user, ?string $type): array |
||||
{ |
||||
if (null === $type || $user instanceof ($type)) { |
||||
return [$user::class]; |
||||
} |
||||
|
||||
$types = explode('|', $type); |
||||
foreach ($types as $candidate) { |
||||
if ($user instanceof $candidate) { |
||||
return [$user::class, $candidate]; |
||||
} |
||||
} |
||||
|
||||
return []; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(resolveUser(new AdminUser(), UserInterface::class)); |
||||
var_dump(resolveUser(new AdminUser(), GuestUser::class.'|'.UserInterface::class)); |
||||
var_dump(resolveUser(new GuestUser(), UserInterface::class)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(1) { |
||||
[0]=> |
||||
string(9) "AdminUser" |
||||
} |
||||
array(2) { |
||||
[0]=> |
||||
string(9) "AdminUser" |
||||
[1]=> |
||||
string(13) "UserInterface" |
||||
} |
||||
array(0) { |
||||
} |
||||
@ -0,0 +1,35 @@ |
||||
--TEST-- |
||||
Symfony Serializer pattern: static local coalesce assignment with ternary object cache |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class LocalResolver |
||||
{ |
||||
public function resolve(string $value): string |
||||
{ |
||||
return 'resolved:'.$value; |
||||
} |
||||
} |
||||
|
||||
function resolveWhenAvailable(?string $value, bool $enabled): ?string |
||||
{ |
||||
static $resolver; |
||||
|
||||
if (null !== $value && $resolver ??= $enabled && class_exists(LocalResolver::class) ? new LocalResolver() : false) { |
||||
return $resolver->resolve($value); |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(resolveWhenAvailable(null, true)); |
||||
var_dump(resolveWhenAvailable('first', true)); |
||||
var_dump(resolveWhenAvailable('second', false)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
NULL |
||||
string(14) "resolved:first" |
||||
string(15) "resolved:second" |
||||
Loading…
Reference in new issue